diff --git a/assume/common/base.py b/assume/common/base.py index 10278e3c4..b1716853b 100644 --- a/assume/common/base.py +++ b/assume/common/base.py @@ -7,16 +7,11 @@ from typing import TypedDict import numpy as np -import pandas as pd +from assume.common.fast_pandas import FastSeries, TensorFastSeries from assume.common.forecasts import Forecaster from assume.common.market_objects import MarketConfig, Orderbook, Product -try: - import torch as th -except ImportError: - th = None - class BaseStrategy: pass @@ -26,22 +21,12 @@ class BaseUnit: """ A base class for a unit. This class is used as a foundation for all units. - Attributes: - id (str): The ID of the unit. - unit_operator (str): The operator of the unit. - technology (str): The technology of the unit. - bidding_strategies (dict[str, BaseStrategy]): The bidding strategies of the unit. - index (pandas.DatetimeIndex): The index of the unit. - node (str, optional): The node of the unit. Defaults to "". - forecaster (Forecaster, optional): The forecast of the unit. Defaults to None. - **kwargs: Additional keyword arguments. - Args: id (str): The ID of the unit. unit_operator (str): The operator of the unit. technology (str): The technology of the unit. bidding_strategies (dict[str, BaseStrategy]): The bidding strategies of the unit. - index (pandas.DatetimeIndex): The index of the unit. + index (FastIndex): The index of the unit. node (str, optional): The node of the unit. Defaults to "". forecaster (Forecaster, optional): The forecast of the unit. Defaults to None. location (tuple[float, float], optional): The location of the unit. Defaults to (0.0, 0.0). @@ -55,38 +40,41 @@ def __init__( unit_operator: str, technology: str, bidding_strategies: dict[str, BaseStrategy], - index: pd.DatetimeIndex, + forecaster: Forecaster, node: str = "node0", - forecaster: Forecaster = None, location: tuple[float, float] = (0.0, 0.0), **kwargs, ): self.id = id self.unit_operator = unit_operator self.technology = technology + self.bidding_strategies: dict[str, BaseStrategy] = bidding_strategies + self.forecaster = forecaster + self.index = forecaster.index + self.node = node self.location = location - self.bidding_strategies: dict[str, BaseStrategy] = bidding_strategies - self.index = index - self.outputs = defaultdict(lambda: pd.Series(0.0, index=self.index)) - # series does not like to convert from tensor to float otherwise - # RL data stored as lists to simplify storing to the buffer - self.outputs["rl_observations"] = [] - self.outputs["rl_actions"] = [] - self.outputs["rl_rewards"] = [] + self.outputs = defaultdict(lambda: FastSeries(value=0.0, index=self.index)) + # series does not like to convert from tensor to float otherwise # some data is stored as series to allow to store it in the outputs - self.outputs["actions"] = pd.Series(0.0, index=self.index, dtype=object) - self.outputs["exploration_noise"] = pd.Series( - 0.0, index=self.index, dtype=object - ) - self.outputs["reward"] = pd.Series(0.0, index=self.index, dtype=object) + # check if any bidding strategy is using the RL strategy + if any( + isinstance(strategy, LearningStrategy) + for strategy in self.bidding_strategies.values() + ): + self.outputs["actions"] = TensorFastSeries(value=0.0, index=self.index) + self.outputs["exploration_noise"] = TensorFastSeries( + value=0.0, + index=self.index, + ) + self.outputs["reward"] = FastSeries(value=0.0, index=self.index) - if forecaster: - self.forecaster = forecaster - else: - self.forecaster = defaultdict(lambda: pd.Series(0.0, index=self.index)) + # RL data stored as lists to simplify storing to the buffer + self.outputs["rl_observations"] = [] + self.outputs["rl_actions"] = [] + self.outputs["rl_rewards"] = [] def calculate_bids( self, @@ -128,12 +116,12 @@ def calculate_bids( return bids - def calculate_marginal_cost(self, start: pd.Timestamp, power: float) -> float: + def calculate_marginal_cost(self, start: datetime, power: float) -> float: """ - Calculates the marginal cost for the given power. + Calculates the marginal cost for the given power.` Args: - start (pandas.Timestamp): The start time of the dispatch. + start (datetime.datetime): The start time of the dispatch. power (float): The power output of the unit. Returns: @@ -192,19 +180,23 @@ def calculate_generation_cost( start = self.index[0] product_type_mc = product_type + "_marginal_costs" - product_data = self.outputs[product_type].loc[start:end] - - marginal_costs = product_data.index.map( - lambda t: self.calculate_marginal_cost(t, product_data.loc[t]) - ) - new_values = np.abs(marginal_costs * product_data.values) + # Adjusted code for accessing product data and mapping over the index + product_data = self.outputs[product_type].loc[ + start:end + ] # Slicing directly without `.loc` + + marginal_costs = [ + self.calculate_marginal_cost(t, product_data[idx]) + for idx, t in enumerate(self.index[start:end]) + ] + new_values = np.abs(marginal_costs * product_data) self.outputs[product_type_mc].loc[start:end] = new_values def execute_current_dispatch( self, - start: pd.Timestamp, - end: pd.Timestamp, - ) -> pd.Series: + start: datetime, + end: datetime, + ) -> np.array: """ Checks if the total dispatch plan is feasible. @@ -218,7 +210,7 @@ def execute_current_dispatch( Returns: The volume of the unit within the given time range. """ - return self.outputs["energy"][start:end] + return self.outputs["energy"].loc[start:end] def get_output_before(self, dt: datetime, product_type: str = "energy") -> float: """ @@ -267,21 +259,21 @@ def calculate_cashflow(self, product_type: str, orderbook: Orderbook): end_excl = end - self.index.freq if isinstance(order["accepted_volume"], dict): - cashflow = [ - float(order["accepted_price"][i] * order["accepted_volume"][i]) - for i in order["accepted_volume"].keys() - ] - self.outputs[f"{product_type}_cashflow"].loc[start:end_excl] += ( - cashflow * self.index.freq.n + cashflow = np.array( + [ + float(order["accepted_price"][i] * order["accepted_volume"][i]) + for i in order["accepted_volume"].keys() + ] ) else: cashflow = float( order.get("accepted_price", 0) * order.get("accepted_volume", 0) ) - elapsed_intervals = (end - start) / pd.Timedelta(self.index.freq) - self.outputs[f"{product_type}_cashflow"].loc[start:end_excl] += ( - cashflow * elapsed_intervals - ) + + elapsed_intervals = (end - start) / self.index.freq + self.outputs[f"{product_type}_cashflow"].loc[start:end_excl] += ( + cashflow * elapsed_intervals + ) def get_starting_costs(self, op_time: int) -> float: """ @@ -322,18 +314,18 @@ class SupportsMinMax(BaseUnit): min_down_time: int = 0 def calculate_min_max_power( - self, start: pd.Timestamp, end: pd.Timestamp, product_type: str = "energy" - ) -> tuple[pd.Series, pd.Series]: + self, start: datetime, end: datetime, product_type: str = "energy" + ) -> tuple[np.array, np.array]: """ Calculates the min and max power for the given time period. Args: - start (pandas.Timestamp): The start time of the dispatch. - end (pandas.Timestamp): The end time of the dispatch. + start (datetime.datetime): The start time of the dispatch. + end (datetime.datetime): The end time of the dispatch. product_type (str): The product type of the unit. Returns: - tuple[pandas.Series, pandas.Series]: The min and max power for the given time period. + tuple[np.array, np.array]: The min and max power for the given time period. """ def calculate_ramp( @@ -355,7 +347,6 @@ def calculate_ramp( Returns: float: The corrected possible power to offer according to ramping restrictions. """ - # was off before, but should be on now and min_down_time is not reached if power > 0 and op_time < 0 and op_time > -self.min_down_time: power = 0 @@ -383,20 +374,6 @@ def calculate_ramp( ) return power - def get_clean_spread(self, prices: pd.DataFrame) -> float: - """ - Returns the clean spread for the given prices. - - Args: - prices (pandas.DataFrame): The prices. - - Returns: - float: The clean spread for the given prices. - """ - emission_cost = self.emission_factor * prices["co"].mean() - fuel_cost = prices[self.technology.replace("_combined", "")].mean() - return (fuel_cost + emission_cost) / self.efficiency - def get_operation_time(self, start: datetime) -> int: """ Returns the time the unit is operating (positive) or shut down (negative). @@ -405,24 +382,32 @@ def get_operation_time(self, start: datetime) -> int: start (datetime.datetime): The start time. Returns: - int: The operation time. + int: The operation time as a positive integer if operating, or negative if shut down. """ - before = start - self.index.freq + # Set the time window based on max of min operating/down time + max_time = max(self.min_operating_time, self.min_down_time, 1) + begin = max(start - self.index.freq * max_time, self.index[0]) + end = start - self.index.freq - max_time = max(self.min_operating_time, self.min_down_time) - begin = start - self.index.freq * max_time - end = before - arr = self.outputs["energy"][begin:end][::-1] > 0 - if len(arr) < 1: + if start <= self.index[0]: # before start of index return max_time - is_off = not arr.iloc[0] + + # Check energy output in the defined time window, reversed for most recent state + arr = (self.outputs["energy"].loc[begin:end] > 0)[::-1] + + # Determine initial state (off if the first period shows zero energy output) + is_off = not arr[0] run = 0 + + # Count consecutive periods with the same status, break on change for val in arr: - if val == is_off: + if val != (not is_off): # Stop if the state changes break run += 1 - return (-1) ** is_off * run + + # Return positive time if operating, negative if shut down + return -run if is_off else run def get_average_operation_times(self, start: datetime) -> tuple[float, float]: """ @@ -440,14 +425,14 @@ def get_average_operation_times(self, start: datetime) -> tuple[float, float]: op_series = [] before = start - self.index.freq - arr = self.outputs["energy"][self.index[0] : before][::-1] > 0 + arr = self.outputs["energy"].loc[self.index[0] : before][::-1] > 0 if len(arr) < 1: # before start of index return max(self.min_operating_time, 1), min(-self.min_down_time, -1) op_series = [] - status = arr.iloc[0] + status = arr[0] run = 0 for val in arr: if val == status: @@ -537,33 +522,33 @@ class SupportsMinMaxCharge(BaseUnit): efficiency_discharge: float def calculate_min_max_charge( - self, start: pd.Timestamp, end: pd.Timestamp, product_type="energy" - ) -> tuple[pd.Series, pd.Series]: + self, start: datetime, end: datetime, product_type="energy" + ) -> tuple[np.array, np.array]: """ Calculates the min and max charging power for the given time period. Args: - start (pandas.Timestamp): The start time of the dispatch. - end (pandas.Timestamp): The end time of the dispatch. + start (datetime.datetime): The start time of the dispatch. + end (datetime.datetime): The end time of the dispatch. product_type (str, optional): The product type of the unit. Defaults to "energy". Returns: - tuple[pandas.Series, pandas.Series]: The min and max charging power for the given time period. + tuple[np.array, np.array]: The min and max charging power for the given time period. """ def calculate_min_max_discharge( - self, start: pd.Timestamp, end: pd.Timestamp, product_type="energy" - ) -> tuple[pd.Series, pd.Series]: + self, start: datetime, end: datetime, product_type="energy" + ) -> tuple[np.array, np.array]: """ Calculates the min and max discharging power for the given time period. Args: - start (pandas.Timestamp): The start time of the dispatch. - end (pandas.Timestamp): The end time of the dispatch. + start (datetime.datetime): The start time of the dispatch. + end (datetime.datetime): The end time of the dispatch. product_type (str, optional): The product type of the unit. Defaults to "energy". Returns: - tuple[pandas.Series, pandas.Series]: The min and max discharging power for the given time period. + tuple[np.array, np.array]: The min and max discharging power for the given time period. """ def get_soc_before(self, dt: datetime) -> float: @@ -583,20 +568,6 @@ def get_soc_before(self, dt: datetime) -> float: else: return self.outputs["soc"].at[dt - self.index.freq] - def get_clean_spread(self, prices: pd.DataFrame) -> float: - """ - Returns the clean spread for the given prices. - - Args: - prices (pandas.DataFrame): The prices. - - Returns: - float: The clean spread for the given prices. - """ - emission_cost = self.emission_factor * prices["co"].mean() - fuel_cost = prices[self.technology.replace("_combined", "")].mean() - return (fuel_cost + emission_cost) / self.efficiency_charge - def calculate_ramp_discharge( self, previous_power: float, diff --git a/assume/common/fast_pandas.py b/assume/common/fast_pandas.py new file mode 100644 index 000000000..8071a37d7 --- /dev/null +++ b/assume/common/fast_pandas.py @@ -0,0 +1,1282 @@ +# SPDX-FileCopyrightText: ASSUME Developers +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from datetime import datetime, timedelta +from functools import lru_cache + +import numpy as np +import pandas as pd +from pandas.api.types import is_datetime64_any_dtype + +try: + import torch as th +except ImportError: + th = None + + +class FastIndex: + """ + A fast, memory-efficient datetime index similar to pandas DatetimeIndex. + + This class manages a range of datetime objects with a specified frequency, + providing efficient indexing, slicing (both integer and datetime-based), + and membership checking with alignment tolerance. + """ + + def __init__( + self, + start: datetime | str, + end: datetime | str = None, + freq: timedelta | str = timedelta(hours=1), + periods: int = None, + ): + """ + Initialize the FastIndex. + + Parameters: + start (datetime | str): The start datetime or its string representation. + end (datetime | str, optional): The end datetime or its string representation. Defaults to None. + freq (timedelta | str, optional): The frequency of the index. Can be a timedelta or pandas-style string. + Defaults to timedelta(hours=1). + periods (int, optional): Number of periods in the index. Either `end` or `periods` must be provided. + """ + self._start = self._convert_to_datetime(start) + if end is None and periods is None: + raise ValueError("Either 'end' or 'periods' must be specified") + + self._freq = self._parse_frequency(freq) + self._freq_seconds = self._freq.total_seconds() + + if periods is not None: + self._end = self._start + (periods - 1) * self._freq + self._count = periods + else: + self._end = self._convert_to_datetime(end) + total_seconds = (self._end - self._start).total_seconds() + self._count = int(np.floor(total_seconds / self._freq_seconds)) + 1 + + self._tolerance_seconds = 1 + self._date_list = None # Lazy-loaded + + @property + def start(self) -> datetime: + """Get the start datetime of the index.""" + return self._start + + @property + def end(self) -> datetime: + """Get the end datetime of the index.""" + return self._end + + @property + def freq(self) -> timedelta: + """Get the frequency of the index as a timedelta.""" + return self._freq + + @property + def freq_seconds(self) -> float: + """Get the frequency of the index in total seconds.""" + return self._freq_seconds + + @property + def tolerance_seconds(self) -> int: + """Get the tolerance in seconds for date alignment.""" + return self._tolerance_seconds + + def __getitem__(self, item: int | slice): + """ + Retrieve datetime(s) based on the specified index or slice. + + Parameters: + item (int | slice): Index or slice to retrieve. Slices can use integers or datetime values. + + Returns: + datetime | FastIndex: A single datetime object or a new FastIndex for the sliced range. + + Raises: + IndexError: If an integer index is out of range. + TypeError: If `item` is not an integer or slice. + ValueError: If slicing results in an empty range. + """ + if self._date_list is None: + self.get_date_list() + + if isinstance(item, int): + if item < 0: + item += len(self._date_list) + if item < 0 or item >= len(self._date_list): + raise IndexError("Index out of range") + return self._date_list[item] + + elif isinstance(item, slice): + start_idx = ( + self._get_idx_from_date(item.start) + if isinstance(item.start, datetime) + else item.start or 0 + ) + stop_idx = ( + self._get_idx_from_date(item.stop) + 1 + if isinstance(item.stop, datetime) + else item.stop or len(self._date_list) + ) + step = item.step or 1 + + if isinstance(start_idx, int) and start_idx < 0: + start_idx += len(self._date_list) + if isinstance(stop_idx, int) and stop_idx < 0: + stop_idx += len(self._date_list) + + sliced_dates = self._date_list[start_idx:stop_idx:step] + if not sliced_dates: + return [] + + return FastIndex( + start=sliced_dates[0], end=sliced_dates[-1], freq=self._freq + ) + + else: + raise TypeError("Index must be an integer or a slice") + + def __contains__(self, date: datetime) -> bool: + """ + Check if a datetime is within the index range and aligned with the frequency. + + Parameters: + date (datetime.datetime): The datetime to check. + + Returns: + bool: True if the datetime is in the index range and aligned; False otherwise. + """ + if self.start > date or self.end < date: + return False + try: + self._get_idx_from_date(date) + return True + except ValueError: + return False + + def __len__(self) -> int: + """Return the number of datetime points in the index.""" + return self._count + + def __repr__(self) -> str: + """Return a string representation of the FastIndex, including metadata and a date preview.""" + preview_length = 3 # Show first and last 3 dates + date_list = self.get_date_list() + + def format_dates(date_range, date_format="%Y-%m-%d %H:%M:%S"): + return ", ".join(date.strftime(date_format) for date in date_range) + + if len(date_list) <= 2 * preview_length: + preview_str = format_dates(date_list) + else: + preview_str = format_dates(date_list[:preview_length]) + ", ..., " + preview_str += format_dates(date_list[-preview_length:]) + + metadata = ( + f"FastIndex(start={self.start}, end={self.end}, " + f"freq='{self.freq}', dtype=datetime64[ns])" + ) + return f"{metadata}\nDates Preview: [{preview_str}]" + + def __str__(self) -> str: + """Return an informal string representation of the FastIndex.""" + return self.__repr__() + + @lru_cache(maxsize=100) + def get_date_list( + self, start: datetime | None = None, end: datetime | None = None + ) -> list[datetime]: + """ + Generate a list of datetime objects within the specified range. + + Parameters: + start (datetime | None, optional): Start datetime for the subset. Defaults to the beginning of the index. + end (datetime | None, optional): End datetime for the subset. Defaults to the end of the index. + + Returns: + list[datetime]: A list of datetime objects representing the specified range. + """ + if self._date_list is None: + total_dates = np.arange(self._count) * self._freq_seconds + self._date_list = [self._start + timedelta(seconds=s) for s in total_dates] + + start_idx = self._get_idx_from_date(start or self.start) + end_idx = self._get_idx_from_date(end or self.end) + 1 + return self._date_list[start_idx:end_idx] + + def as_datetimeindex(self) -> pd.DatetimeIndex: + """ + Convert the FastIndex to a pandas DatetimeIndex. + + Returns: + pd.DatetimeIndex: A pandas DatetimeIndex representing the FastIndex. + """ + # Retrieve the datetime range using get_date_list + datetimes = self.get_date_list() + # Convert to pandas DatetimeIndex + return pd.DatetimeIndex(pd.to_datetime(datetimes), name="FastIndex") + + @lru_cache(maxsize=1000) + def _get_idx_from_date(self, date: datetime) -> int: + """ + Convert a datetime to its corresponding index in the range. + + Parameters: + date (datetime.datetime): The datetime to convert. + + Returns: + int: The index of the datetime in the index range. + + Raises: + KeyError: If the input `date` is None. + ValueError: If the `date` is not aligned with the frequency within tolerance. + """ + if date is None: + raise KeyError("Date cannot be None. Please provide a valid datetime.") + + delta_seconds = (date - self.start).total_seconds() + remainder = delta_seconds % self.freq_seconds + + if remainder > self.tolerance_seconds and remainder < ( + self.freq_seconds - self.tolerance_seconds + ): + raise ValueError( + f"Date {date} is not aligned with frequency {self.freq_seconds} seconds. " + f"Allowed tolerance: {self.tolerance_seconds} seconds." + ) + + return round(delta_seconds / self.freq_seconds) + + @staticmethod + def _convert_to_datetime(value: datetime | str) -> datetime: + """Convert input to datetime if it's not already.""" + return value if isinstance(value, datetime) else pd.to_datetime(value) + + @staticmethod + def _parse_frequency(freq: timedelta | str) -> timedelta: + """ + Parse a frequency input into a timedelta. + + Parameters: + freq (timedelta | str): Frequency in timedelta or pandas-style string format. + + Returns: + timedelta: The parsed frequency. + + Raises: + TypeError: If the input type is not supported. + ValueError: If the string format is invalid. + """ + if isinstance(freq, timedelta): + return freq + if isinstance(freq, str): + try: + if freq.isalpha(): + freq = f"1{freq}" + return pd.to_timedelta(freq) + except ValueError as e: + raise ValueError(f"Invalid frequency string: {freq}. Error: {e}") + raise TypeError("Frequency must be a string or timedelta") + + +class FastSeries: + """ + A fast, memory-efficient replacement for pandas Series with a FastIndex. + + This class leverages NumPy arrays for data storage to enhance performance + during market simulations. It supports lazy initialization, vectorized + operations, and partial compatibility with pandas Series for ease of use. + + Attributes: + index (FastIndex): The datetime-based index for the series. + data (np.ndarray): The underlying NumPy array storing series values. + name (str): The name of the series. + """ + + def __init__( + self, index: FastIndex, value: float | np.ndarray = 0.0, name: str = "" + ): + """ + Initialize the FastSeries. + + Parameters: + index (FastIndex): The datetime index. + value (float | np.ndarray, optional): Initial value(s) for the data. Defaults to 0.0. + name (str, optional): Name of the series. Defaults to an empty string. + """ + # check that the index is a FastIndex + if not isinstance(index, FastIndex): + raise TypeError("In FastSeries, index must be a FastIndex object.") + + self._index = index + self._name = name + self.loc = self # Allow adjusting loc as well + self.at = self + + count = len(self.index) # Use index length directly + self._data = ( + np.full(count, value, dtype=np.float64) + if isinstance(value, int | float) + else np.array(value, dtype=np.float64) + ) + + @property + def index(self) -> FastIndex: + """Get the FastIndex of the series.""" + return self._index + + @property + def start(self) -> datetime: + """Get the start datetime of the series.""" + return self._index.start + + @property + def end(self) -> datetime: + """Get the end datetime of the series.""" + return self._index.end + + @property + def freq(self) -> timedelta: + """Get the frequency of the series as a timedelta.""" + return self._index.freq + + @property + def freq_seconds(self) -> float: + """Get the frequency of the series in total seconds.""" + return self._index.freq_seconds + + @property + def name(self) -> str: + """Get the name of the series.""" + return self._name + + @property + def data(self) -> np.ndarray: + """ + Access the underlying data array. + + Returns: + np.ndarray: The data array. + """ + return self._data + + @data.setter + def data(self, value: np.ndarray): + """ + Set the underlying data array. + + Parameters: + value (np.ndarray): The new data array. + """ + if value.shape[0] != len(self.index): + raise ValueError("Data length must match index length.") + self._data = value + + @property + def dtype(self) -> np.dtype: + """ + Get the data type of the series. + + Returns: + np.dtype: The data type of the underlying NumPy array. + """ + return self.data.dtype + + @property + def iloc(self): + """ + Integer-based indexing property. + + Returns: + FastSeriesILocIndexer: Indexer for integer-based access. + """ + return FastSeriesILocIndexer(self) + + @property + def iat(self): + """ + Integer-based single-item access property. + + Returns: + FastSeriesIatIndexer: Indexer for integer-based single-element access. + """ + return FastSeriesIatIndexer(self) + + def __getitem__( + self, item: datetime | slice | list | pd.Index | pd.Series | np.ndarray | str + ): + """ + Retrieve item(s) from the series using datetime or label-based indexing. + + Parameters: + item (datetime | slice | list | pd.Index | pd.Series | np.ndarray | str): + The key(s) to retrieve. + + Returns: + float | np.ndarray: The retrieved value(s). + + Raises: + TypeError: If the index type is unsupported. + ValueError: If dates are not aligned within tolerance. + """ + if isinstance(item, slice): + # Handle slicing with datetime start/stop + start_idx = ( + self.index._get_idx_from_date(item.start) + if item.start is not None + else 0 + ) + stop_idx = ( + self.index._get_idx_from_date(item.stop) + 1 + if item.stop is not None + else len(self.data) + ) + return self.data[start_idx:stop_idx] + + elif isinstance( + item, (list | pd.Index | pd.DatetimeIndex | np.ndarray | pd.Series) + ): + # Handle list-like datetime-based inputs + dates = self._convert_to_datetime_array(item) + delta_seconds = np.array( + [(d - self.index.start).total_seconds() for d in dates] + ) + indices = (delta_seconds / self.index.freq_seconds).round().astype(int) + remainders = delta_seconds % self.index.freq_seconds + + if not np.all(remainders <= self.index.tolerance_seconds): + raise ValueError( + "One or more dates are not aligned with the index frequency." + ) + return self.data[indices] + + elif isinstance(item, str): + # Handle string input + date = pd.to_datetime(item).to_pydatetime() + return self.data[self.index._get_idx_from_date(date)] + + elif isinstance(item, datetime): + # Handle datetime input + return self.data[self.index._get_idx_from_date(item)] + + else: + raise TypeError( + f"Unsupported index type: {type(item)}. Must be datetime, slice, list, " + "pandas Index, NumPy array, pandas Series, or string." + ) + + def __setitem__( + self, + item: datetime | slice | list | pd.Index | pd.Series | np.ndarray | str, + value: float | np.ndarray, + ): + """ + Assign value(s) to item(s) in the series. + + Parameters: + item (datetime | slice | list | pd.Index | pd.Series | np.ndarray | str): + The key(s) to set. + value (float | np.ndarray): The value(s) to assign. + + Raises: + TypeError: If the index type is unsupported. + ValueError: If lengths of indices and values do not match or dates are not aligned within tolerance. + """ + if isinstance(item, slice): + # Handle slicing + start_idx = ( + self.index._get_idx_from_date(item.start) + if isinstance(item.start, datetime) + else ( + len(self.data) + item.start + if item.start is not None and item.start < 0 + else 0 + ) + ) + stop_idx = ( + self.index._get_idx_from_date(item.stop) + 1 + if isinstance(item.stop, datetime) + else ( + len(self.data) + item.stop + if item.stop is not None and item.stop < 0 + else len(self.data) + ) + ) + + # Assign values to the slice + if np.isscalar(value) or len(self.data[start_idx:stop_idx]) == len(value): + self.data[start_idx:stop_idx] = value + else: + raise ValueError( + f"Length of values ({len(value)}) does not match slice length ({stop_idx - start_idx})." + ) + + elif isinstance( + item, (list | pd.Index | pd.DatetimeIndex | np.ndarray | pd.Series) + ): + if ( + len(item) == len(self.data) + and item[0] == self.index.start + and item[-1] == self.index.end + ): + self.data = np.array(value) + else: + if isinstance(value, pd.Series): + for idx, i in enumerate(item): + start = self.index._get_idx_from_date(i) + self.data[start] = value.iloc[idx] + elif isinstance(value, list | np.ndarray): + for idx, i in enumerate(item): + start = self.index._get_idx_from_date(i) + self.data[start] = value[idx] + else: + for i in item: + start = self.index._get_idx_from_date(i) + self.data[start] = value + + elif isinstance(item, datetime | str): + # Handle single datetime or string + date = ( + pd.to_datetime(item).to_pydatetime() if isinstance(item, str) else item + ) + idx = self.index._get_idx_from_date(date) + self.data[idx] = value + + else: + raise TypeError( + f"Unsupported index type: {type(item)}. Must be datetime, slice, list, " + "pandas Index, NumPy array, pandas Series, or string." + ) + + def __add__(self, other: int | float | np.ndarray): + return self._arithmetic_operation(other, "add") + + def __sub__(self, other: int | float | np.ndarray): + return self._arithmetic_operation(other, "sub") + + def __mul__(self, other: int | float | np.ndarray): + return self._arithmetic_operation(other, "mul") + + def __truediv__(self, other: int | float | np.ndarray): + return self._arithmetic_operation(other, "truediv") + + # Support for in-place operations + def __iadd__(self, other: int | float | np.ndarray): + self.data = self.__add__(other).data + return self + + def __isub__(self, other: int | float | np.ndarray): + self.data = self.__sub__(other).data + return self + + def __imul__(self, other: int | float | np.ndarray): + self.data = self.__mul__(other).data + return self + + def __itruediv__(self, other: int | float | np.ndarray): + self.data = self.__truediv__(other).data + return self + + def __neg__(self): + """ + Negate all values in the series. + + Returns: + FastSeries: A new FastSeries with negated values. + """ + result = self.copy() + result.data = -self.data + return result + + def __abs__(self): + result = self.copy() + result.data = abs(self.data) + return result + + # Reverse Arithmetic Operations + def __radd__(self, other: int | float | np.ndarray): + return self.__add__(other) + + def __rsub__(self, other: int | float | np.ndarray): + result = self.copy() + result.data = other - self.data + return result + + def __rmul__(self, other: int | float | np.ndarray): + return self.__mul__(other) + + def __rtruediv__(self, other: int | float | np.ndarray): + result = self.copy() + result.data = other / self.data + return result + + # Comparison Operations + def __gt__(self, other: int | float | np.ndarray) -> np.ndarray: + """ + Greater than comparison. + + Parameters: + other (int | float | np.ndarray): The value(s) to compare against. + + Returns: + np.ndarray: Boolean array where True indicates the series value is greater. + """ + return self.data > other + + def __lt__(self, other: int | float | np.ndarray) -> np.ndarray: + """ + Less than comparison. + + Parameters: + other (int | float | np.ndarray): The value(s) to compare against. + + Returns: + np.ndarray: Boolean array where True indicates the series value is less. + """ + return self.data < other + + def __ge__(self, other: int | float | np.ndarray) -> np.ndarray: + """ + Greater than or equal to comparison. + + Parameters: + other (int | float | np.ndarray): The value(s) to compare against. + + Returns: + np.ndarray: Boolean array where True indicates the series value is greater or equal. + """ + return self.data >= other + + def __le__(self, other: int | float | np.ndarray) -> np.ndarray: + """ + Less than or equal to comparison. + + Parameters: + other (int | float | np.ndarray): The value(s) to compare against. + + Returns: + np.ndarray: Boolean array where True indicates the series value is less or equal. + """ + return self.data <= other + + def __eq__(self, other: int | float | np.ndarray) -> np.ndarray: + """ + Equality comparison. + + Parameters: + other (int | float | np.ndarray): The value(s) to compare against. + + Returns: + np.ndarray: Boolean array where True indicates the series value is equal. + """ + return self.data == other + + def __ne__(self, other: int | float | np.ndarray) -> np.ndarray: + """ + Not equal comparison. + + Parameters: + other (int | float | np.ndarray): The value(s) to compare against. + + Returns: + np.ndarray: Boolean array where True indicates the series value is not equal. + """ + return self.data != other + + def __len__(self) -> int: + """ + Get the number of data points in the series. + + Returns: + int: The length of the series. + """ + return len(self.data) + + def __repr__(self, preview_length: int = 3) -> str: + """ + Official string representation of the FastSeries, showing metadata and a sample of data. + + Parameters: + preview_length (int, optional): Number of entries to show from the start and end. Defaults to 3. + + Returns: + str: String representation of the FastSeries. + """ + if len(self) == 0: + return f"FastSeries(name='{self.name}', start={self.start}, end={self.end}, freq='{self.freq}', dtype={self.dtype})\n\nData Preview:\n[Empty Series]" + + data_preview = np.concatenate( + (self.data[:preview_length], self.data[-preview_length:]) + ) + dates_preview = ( + self.index.get_date_list()[:preview_length] + + self.index.get_date_list()[-preview_length:] + ) + + preview_str = "\n".join( + f"{date}: {value}" for date, value in zip(dates_preview, data_preview) + ) + + metadata = ( + f"FastSeries(name='{self.name}', start={self.start}, end={self.end}, " + f"freq='{self.freq}', dtype={self.dtype})" + ) + + return f"{metadata}\n\nData Preview:\n{preview_str}\n{'...' if len(self) > 2 * preview_length else ''}" + + def __str__(self) -> str: + """ + Informal string representation of the FastSeries, identical to __repr__. + + Returns: + str: String representation of the FastSeries. + """ + return self.__repr__() + + # Aggregation Methods + def mean(self) -> float: + """ + Calculate the mean of the series. + + Returns: + float: The mean value. + """ + return self.data.mean() + + def sum(self) -> float: + """ + Calculate the sum of the series. + + Returns: + float: The sum of all values. + """ + return self.data.sum() + + def min(self) -> float: + """ + Find the minimum value in the series. + + Returns: + float: The minimum value. + """ + return self.data.min() + + def max(self) -> float: + """ + Find the maximum value in the series. + + Returns: + float: The maximum value. + """ + return self.data.max() + + def std(self) -> float: + """ + Calculate the standard deviation of the series. + + Returns: + float: The standard deviation. + """ + return self.data.std() + + def median(self) -> float: + """ + Calculate the median of the series. + + Returns: + float: The median value. + """ + return np.median(self.data) + + def copy(self, deep: bool = False): + """ + Create a copy of the FastSeries. + + Parameters: + deep (bool, optional): If True, perform a deep copy of the data array. Defaults to False. + + Returns: + FastSeries: A new FastSeries instance with copied data and metadata. + """ + copied_data = self._data.copy() if deep else self._data.view() + return FastSeries( + index=self.index, + value=copied_data, + name=self.name, + ) + + def as_df( + self, name: str = None, start: datetime = None, end: datetime = None + ) -> pd.DataFrame: + """ + Convert the FastSeries to a pandas DataFrame. + + Parameters: + name (str | None): Name of the DataFrame column. Defaults to None. + start (datetime | None): Start datetime for the DataFrame. Defaults to None. + end (datetime | None): End datetime for the DataFrame. Defaults to None. + + Returns: + pd.DataFrame: DataFrame representation of the series. + """ + data_slice = self[start:end] + index = pd.to_datetime(self.index.get_date_list(start, end)) + return pd.DataFrame( + data_slice, index=index, columns=[name if name else self.name] + ) + + def as_pd_series( + self, name: str = None, start: datetime = None, end: datetime = None + ) -> pd.Series: + """ + Convert the FastSeries to a pandas Series. + + Parameters: + name (str | None): Name of the Series. Defaults to None. + start (datetime | None): Start datetime for the Series. Defaults to None. + end (datetime | None): End datetime for the Series. Defaults to None. + + Returns: + pd.Series: pandas Series representation of the FastSeries. + """ + # Slice the data within the specified range + data_slice = self[start:end] + # Generate the corresponding index + index = pd.to_datetime(self.index.get_date_list(start, end)) + # Create and return the pandas Series + return pd.Series(data_slice, index=index, name=name if name else self.name) + + @staticmethod + def from_pandas_series(series: pd.Series): + """ + Create a FastSeries from a pandas Series. + + Parameters: + series (pd.Series): The pandas Series to convert. + + Returns: + FastSeries: The converted FastSeries object. + + Raises: + ValueError: If the series has fewer than two index entries to infer frequency or frequency cannot be inferred. + """ + if series.empty: + raise ValueError("Cannot create FastSeries from an empty pandas Series.") + + freq = pd.infer_freq(series.index) + if freq is None: + raise ValueError("Cannot infer frequency from the series index.") + + if freq.isalpha(): # Ensure numeric format for frequency + freq = f"1{freq}" + + index = FastIndex( + start=series.index[0].to_pydatetime(), + end=series.index[-1].to_pydatetime(), + freq=freq, + ) + return FastSeries( + index=index, + value=series.values, + name=series.name or "", + ) + + def __iter__(self): + """ + Make FastSeries iterable by iterating over the stored data. + + Yields: + The elements of the series data (e.g., float, tensor). + """ + return iter(self._data) + + # Helper method to check index alignment + def _index_aligned_with(self, other: "FastSeries") -> bool: + """ + Check if this series is aligned with another FastSeries. + + Parameters: + other (FastSeries): The other series to check alignment with. + + Returns: + bool: True if the indices match, False otherwise. + """ + aligned = ( + self.start == other.start + and self.end == other.end + and self.freq == other.freq + and len(self.data) == len(other.data) + ) + if not aligned: + print( # Replace with logging if needed + f"Indices are not aligned:\n" + f"Self: start={self.start}, end={self.end}, freq={self.freq}, len={len(self.data)}\n" + f"Other: start={other.start}, end={other.end}, freq={other.freq}, len={len(other.data)}" + ) + return aligned + + def _convert_to_datetime_array( + self, item: list | pd.Index | pd.Series | np.ndarray + ) -> np.ndarray: + """ + Convert input to a NumPy array of datetime objects. + + Parameters: + item (list | pd.Index | pd.Series | np.ndarray): + A collection of datetimes (e.g., list, pandas Index, Series, or NumPy array). + + Returns: + np.ndarray: Array of datetime objects. + + Raises: + ValueError: If the input cannot be converted to datetime. + """ + try: + if isinstance(item, pd.Series): + if is_datetime64_any_dtype(item.index): + item = item.index + else: + item = item.values + + return pd.to_datetime(item).to_pydatetime() + except Exception as e: + raise ValueError( + f"Cannot convert {type(item)} to a NumPy array of datetime objects. Ensure the input is " + f"a list, pandas Index, Series, or NumPy array of datetimes. Original error: {e}" + ) + + # Helper for arithmetic operations + def _arithmetic_operation(self, other: int | float | np.ndarray, op: str): + """ + Perform an arithmetic operation on the series. + + Parameters: + other (int | float | np.ndarray | FastSeries): The value(s) to operate on. + op (str): The operation to perform ('add', 'sub', 'mul', 'truediv'). + + Returns: + FastSeries: A new FastSeries with the result of the operation. + + Raises: + ValueError: If the indices do not align for FastSeries. + TypeError: If the `other` type is unsupported. + """ + result = self.copy() + if isinstance(other, int | float | np.ndarray): + result.data = getattr(self.data, f"__{op}__")(other) + elif isinstance(other, FastSeries): + if self._index_aligned_with(other): + result.data = getattr(self.data, f"__{op}__")(other.data) + else: + raise ValueError(f"Cannot perform {op}: Series indices do not match") + else: + raise TypeError(f"Unsupported type for {op}: {type(other)}") + return result + + +class FastSeriesILocIndexer: + def __init__(self, series: FastSeries): + self._series = series + + def __getitem__(self, item: int | slice) -> float | np.ndarray: + """ + Retrieve item(s) using integer-based indexing. + + Parameters: + item (int | slice): The integer index or slice. + + Returns: + float | np.ndarray: The retrieved value(s). + + Raises: + IndexError: If the index is out of bounds. + TypeError: If the index type is unsupported. + """ + if isinstance(item, int): + if item < 0 or item >= len(self._series): + raise IndexError( + f"Index {item} is out of bounds for series of length {len(self._series)}" + ) + return self._series._data[item] + + elif isinstance(item, slice): + start = item.start or 0 + stop = item.stop or len(self._series) + step = item.step or 1 + + if start < 0: + start += len(self._series) + if stop < 0: + stop += len(self._series) + + start = max(0, start) + stop = min(len(self._series), stop) + + return self._series._data[start:stop:step] + + else: + raise TypeError( + f"Unsupported index type for iloc: {type(item)}. Must be int or slice." + ) + + def __setitem__(self, item: int | slice, value: float | np.ndarray): + """ + Assign value(s) using integer-based indexing. + + Parameters: + item (int | slice): The integer index or slice. + value (float | np.ndarray): The value(s) to assign. + + Raises: + IndexError: If the index is out of bounds. + TypeError: If the index type is unsupported. + ValueError: If the length of the value does not match the slice length. + """ + if isinstance(item, int): + if item < 0 or item >= len(self._series): + raise IndexError( + f"Index {item} is out of bounds for series of length {len(self._series)}" + ) + self._series._data[item] = value + + elif isinstance(item, slice): + start = item.start or 0 + stop = item.stop or len(self._series) + step = item.step or 1 + + if start < 0: + start += len(self._series) + if stop < 0: + stop += len(self._series) + + start = max(0, start) + stop = min(len(self._series), stop) + + # Assign the values + if np.isscalar(value) or len(self._series._data[start:stop:step]) == len( + value + ): + self._series._data[start:stop:step] = value + else: + raise ValueError( + f"Length of value ({len(value)}) does not match the length of the slice " + f"({len(self._series._data[start:stop:step])})." + ) + else: + raise TypeError( + f"Unsupported index type for iloc: {type(item)}. Must be int or slice." + ) + + +class FastSeriesIatIndexer: + def __init__(self, series: FastSeries): + self._series = series + + def __getitem__(self, item: int) -> float: + """ + Retrieve a single item using integer-based indexing. + + Parameters: + item (int): The integer index. + + Returns: + float: The retrieved value. + + Raises: + IndexError: If the index is out of bounds. + TypeError: If the index is not an integer. + """ + if not isinstance(item, int): + raise TypeError( + f"iat only supports single integer indices, got {type(item)}" + ) + return self._series.iloc[item] + + def __setitem__(self, item: int, value: float): + """ + Assign a value using integer-based indexing. + + Parameters: + item (int): The integer index. + value (float): The value to assign. + + Raises: + IndexError: If the index is out of bounds. + TypeError: If the index is not an integer. + """ + if not isinstance(item, int): + raise TypeError( + f"iat only supports single integer indices, got {type(item)}" + ) + self._series.iloc[item] = value + + +class TensorFastSeries(FastSeries): + """ + A specialized version of FastSeries designed to handle tensors. + """ + + def __init__(self, index: FastIndex, value=None, name: str = ""): + """ + Initialize a TensorFastSeries. + + Parameters: + index (FastIndex): The index for the series. + value (torch.Tensor | float | None): The initial value to populate the series. + If a scalar (e.g., 0.0) is provided, it will be converted to a tensor. + Defaults to None. + name (str, optional): The name of the series. Defaults to "". + """ + super().__init__(index=index, value=None, name=name) + + # Ensure _data is initialized to hold tensors + if value is None: + self._data = [None for _ in range(len(index))] + elif isinstance(value, th.Tensor): + self._data = [value.clone() for _ in range(len(index))] + elif isinstance(value, (int | float)): + self._data = [th.tensor(value) for _ in range(len(index))] + else: + raise TypeError( + f"Unsupported value type: {type(value)}. Must be torch.Tensor, float, or int." + ) + + def __setitem__(self, item: int | datetime | slice, value): + """ + Assign tensor value(s) to item(s) in the series. + + Parameters: + item (int | datetime | slice): The index or slice. + value (th.Tensor): The tensor value(s) to assign. + """ + if isinstance(item, int): + if item < 0 or item >= len(self._data): + raise IndexError( + f"Index {item} is out of bounds for series of length {len(self._data)}" + ) + self._data[item] = value.clone() + elif isinstance(item, slice): + start_idx = item.start or 0 + stop_idx = item.stop or len(self._data) + step = item.step or 1 + slice_length = len(range(start_idx, stop_idx, step)) + + if len(value) != slice_length: + raise ValueError( + f"Length of value ({len(value)}) does not match the length of the slice ({slice_length})." + ) + for i, idx in enumerate(range(start_idx, stop_idx, step)): + self._data[idx] = value[i].clone() + elif isinstance(item, datetime): + idx = self.index._get_idx_from_date(item) + self._data[idx] = value.clone() + else: + raise TypeError( + f"Unsupported index type: {type(item)}. Must be int, slice, or datetime." + ) + + def __getitem__(self, item: int | datetime | slice): + """ + Retrieve tensor(s) from the series. + + Parameters: + item (int | datetime | slice): The index or slice. + + Returns: + th.Tensor | list[th.Tensor]: The retrieved tensor(s). + """ + if isinstance(item, int): + if item < 0 or item >= len(self._data): + raise IndexError( + f"Index {item} is out of bounds for series of length {len(self._data)}" + ) + return self._data[item] + elif isinstance(item, slice): + start_idx = item.start or 0 + stop_idx = item.stop or len(self._data) + step = item.step or 1 + return [self._data[i] for i in range(start_idx, stop_idx, step)] + elif isinstance(item, datetime): + idx = self.index._get_idx_from_date(item) + return self._data[idx] + else: + raise TypeError( + f"Unsupported index type: {type(item)}. Must be int, slice, or datetime." + ) + + def copy(self, deep: bool = False): + """ + Create a copy of the TensorFastSeries. + + Parameters: + deep (bool): If True, perform a deep copy. Defaults to False. + + Returns: + TensorFastSeries: A new instance with copied data. + """ + if deep: + copied_data = [ + tensor.clone() if tensor is not None else None for tensor in self._data + ] + else: + copied_data = self._data[:] + + return TensorFastSeries( + index=self._index, + value=None, # We'll manually set _data below + name=self._name, + )._set_data(copied_data) + + def _set_data(self, data): + """ + Helper method to set data during initialization. + + Parameters: + data (list[th.Tensor]): The data to set. + + Returns: + TensorFastSeries: The modified instance. + """ + self._data = data + return self + + def __repr__(self) -> str: + """ + Return a string representation of the TensorFastSeries. + + Returns: + str: A string describing the series. + """ + preview_length = 3 # Number of items to preview from the start and end + total_length = len(self._data) + + if total_length == 0: + return f"TensorFastSeries(name='{self._name}', length=0, data=[])" + + # Preview a subset of the data + start_preview = self._data[:preview_length] + end_preview = ( + self._data[-preview_length:] if total_length > preview_length else [] + ) + + preview = ( + start_preview + + (["..."] if total_length > 2 * preview_length else []) + + end_preview + ) + + return ( + f"TensorFastSeries(name='{self._name}', length={total_length}, " + f"data={preview})" + ) + + def __str__(self) -> str: + """ + Informal string representation of the FastSeries, identical to __repr__. + + Returns: + str: String representation of the FastSeries. + """ + return self.__repr__() diff --git a/assume/common/forecasts.py b/assume/common/forecasts.py index 5e6d5e98d..11859c230 100644 --- a/assume/common/forecasts.py +++ b/assume/common/forecasts.py @@ -7,6 +7,8 @@ import numpy as np import pandas as pd +from assume.common.fast_pandas import FastIndex, FastSeries + class Forecaster: """ @@ -28,10 +30,10 @@ class Forecaster: """ - def __init__(self, index: pd.Series): + def __init__(self, index: FastIndex): self.index = index - def __getitem__(self, column: str) -> pd.Series: + def __getitem__(self, column: str) -> FastSeries: """ Returns the forecast for a given column. @@ -39,13 +41,13 @@ def __getitem__(self, column: str) -> pd.Series: column (str): The column of the forecast. Returns: - pd.Series: The forecast. + FastSeries: The forecast. This method returns the forecast for a given column as a pandas Series based on the provided index. """ - return pd.Series(0.0, self.index) + return FastSeries(value=0.0, index=self.index) - def get_availability(self, unit: str) -> pd.Series: + def get_availability(self, unit: str) -> FastSeries: """ Returns the availability of a given unit as a pandas Series based on the provided index. @@ -53,7 +55,7 @@ def get_availability(self, unit: str) -> pd.Series: unit (str): The unit. Returns: - pd.Series: The availability of the unit. + FastSeries: The availability of the unit. Example: >>> forecaster = Forecaster(index=pd.Series([1, 2, 3])) @@ -63,7 +65,7 @@ def get_availability(self, unit: str) -> pd.Series: return self[f"availability_{unit}"] - def get_price(self, fuel_type: str) -> pd.Series: + def get_price(self, fuel_type: str) -> FastSeries: """ Returns the price for a given fuel type as a pandas Series or zeros if the type does not exist. @@ -72,7 +74,7 @@ def get_price(self, fuel_type: str) -> pd.Series: fuel_type (str): The fuel type. Returns: - pd.Series: The price of the fuel. + FastSeries: The price of the fuel. Example: >>> forecaster = Forecaster(index=pd.Series([1, 2, 3])) @@ -108,9 +110,10 @@ class CsvForecaster(Forecaster): def __init__( self, index: pd.Series, - powerplants_units: dict[str, pd.Series] = {}, - demand_units: dict[str, pd.Series] = {}, - market_configs: dict[str, pd.Series] = {}, + powerplants_units: pd.DataFrame, + demand_units: pd.DataFrame, + market_configs: dict = {}, + save_path: str = "", *args, **kwargs, ): @@ -120,8 +123,9 @@ def __init__( self.demand_units = demand_units self.market_configs = market_configs self.forecasts = pd.DataFrame(index=index) + self.save_path = save_path - def __getitem__(self, column: str) -> pd.Series: + def __getitem__(self, column: str) -> FastSeries: """ Returns the forecast for a given column. @@ -131,14 +135,14 @@ def __getitem__(self, column: str) -> pd.Series: column (str): The column of the forecast. Returns: - pd.Series: The forecast for the given column. + FastSeries: The forecast for the given column. """ - if column not in self.forecasts.columns: + if column not in self.forecasts.keys(): if "availability" in column: - return pd.Series(1, self.index) - return pd.Series(0.0, self.index) + return FastSeries(value=1.0, index=self.index) + return FastSeries(value=0.0, index=self.index) return self.forecasts[column] @@ -371,7 +375,7 @@ def calculate_marginal_cost(self, pp_series: pd.Series) -> pd.Series: return marginal_cost - def save_forecasts(self, path): + def save_forecasts(self, path=None): """ Saves the forecasts to a csv file located at the specified path. @@ -382,12 +386,39 @@ def save_forecasts(self, path): ValueError: If no forecasts are provided, an error message is logged. """ - try: - self.forecasts.to_csv(f"{path}/forecasts_df.csv", index=True) - except ValueError: - self.logger.error( - f"No forecasts for {self.market_id} provided, so none saved." - ) + path = path or self.save_path + + merged_forecasts = pd.DataFrame(self.forecasts) + merged_forecasts.index = pd.date_range( + start=self.index[0], end=self.index[-1], freq=self.index.freq + ) + merged_forecasts.to_csv(f"{path}/forecasts_df.csv", index=True) + + def convert_forecasts_to_fast_series(self): + """ + Converts all forecasts in self.forecasts (DataFrame) into FastSeries and saves them + in a dictionary. It also converts the self.index to a FastIndex. + """ + # Convert index to FastIndex + inferred_freq = pd.infer_freq(self.index) + if inferred_freq is None: + raise ValueError("Frequency could not be inferred from the index.") + + self.index = FastIndex( + start=self.index[0], end=self.index[-1], freq=inferred_freq + ) + + # Initialize an empty dictionary to store FastSeries + fast_forecasts = {} + + # Convert each column in the forecasts DataFrame to a FastSeries + for column_name in self.forecasts.columns: + # Convert each column in self.forecasts to FastSeries + forecast_series = self.forecasts[column_name] + fast_forecasts[column_name] = FastSeries.from_pandas_series(forecast_series) + + # Replace the DataFrame with the dictionary of FastSeries + self.forecasts = fast_forecasts class RandomForecaster(CsvForecaster): @@ -398,12 +429,12 @@ class RandomForecaster(CsvForecaster): Attributes: index (pandas.Series): The index of the forecasts. - powerplants_units (dict[str, pandas.Series]): The power plants. + powerplants_units (pandas.DataFrame): The power plants. sigma (float): The standard deviation of the noise. Args: index (pandas.Series): The index of the forecasts. - powerplants_units (dict[str, pandas.Series]): The power plants. + powerplants_units (pandas.DataFrame): The power plants. sigma (float): The standard deviation of the noise. Example: @@ -416,15 +447,21 @@ class RandomForecaster(CsvForecaster): def __init__( self, index: pd.Series, - powerplants_units: dict[str, pd.Series] = {}, + powerplants_units: pd.DataFrame, + demand_units: pd.DataFrame, + market_configs: dict = {}, sigma: float = 0.02, *args, **kwargs, ): + super().__init__( + index, powerplants_units, demand_units, market_configs, *args, **kwargs + ) + + self.index = FastIndex(start=index[0], end=index[-1], freq=pd.infer_freq(index)) self.sigma = sigma - super().__init__(index, powerplants_units, *args, **kwargs) - def __getitem__(self, column: str) -> pd.Series: + def __getitem__(self, column: str) -> FastSeries: """ Retrieves forecasted values modified by random noise. @@ -436,14 +473,15 @@ def __getitem__(self, column: str) -> pd.Series: column (str): The column of the forecast. Returns: - pd.Series: The forecast modified by random noise. + FastSeries: The forecast modified by random noise. """ if column not in self.forecasts.columns: - return pd.Series(0.0, self.index) + return FastSeries(value=0.0, index=self.index) noise = np.random.normal(0, self.sigma, len(self.index)) - return self.forecasts[column] * noise + forecast_data = self.forecasts[column].values * noise + return FastSeries(index=self.index, value=forecast_data) class NaiveForecast(Forecaster): @@ -494,13 +532,22 @@ def __init__( **kwargs, ): super().__init__(index) - self.fuel_price = fuel_price - self.availability = availability - self.co2_price = co2_price - self.demand = demand - self.price_forecast = price_forecast + self.index = FastIndex(start=index[0], end=index[-1], freq=pd.infer_freq(index)) + + # Convert attributes to FastSeries if they are not already Series + self.fuel_price = FastSeries( + index=self.index, value=fuel_price, name="fuel_price" + ) + self.availability = FastSeries( + index=self.index, value=availability, name="availability" + ) + self.co2_price = FastSeries(index=self.index, value=co2_price, name="co2_price") + self.demand = FastSeries(index=self.index, value=demand, name="demand") + self.price_forecast = FastSeries( + index=self.index, value=price_forecast, name="price_forecast" + ) - def __getitem__(self, column: str) -> pd.Series: + def __getitem__(self, column: str) -> FastSeries: """ Retrieves forecasted values. @@ -513,22 +560,19 @@ def __getitem__(self, column: str) -> pd.Series: column (str): The column for which forecasted values are requested. Returns: - pd.Series: The forecasted values for the specified column. + FastSeries: The forecasted values for the specified column. """ if "availability" in column: - value = self.availability + return self.availability elif column == "fuel_price_co2": - value = self.co2_price + return self.co2_price elif "fuel_price" in column: - value = self.fuel_price + return self.fuel_price elif "demand" in column: - value = self.demand + return self.demand elif column == "price_EOM": - value = self.price_forecast + return self.price_forecast else: - value = 0 - if isinstance(value, pd.Series): - value.index = self.index - return pd.Series(value, self.index) + return FastSeries(value=0.0, index=self.index) diff --git a/assume/common/outputs.py b/assume/common/outputs.py index ce8d86034..3dd08ac39 100644 --- a/assume/common/outputs.py +++ b/assume/common/outputs.py @@ -20,7 +20,11 @@ from sqlalchemy.exc import DataError, OperationalError, ProgrammingError from assume.common.market_objects import MetaDict -from assume.common.utils import check_for_tensors, separate_orders +from assume.common.utils import ( + calculate_content_size, + check_for_tensors, + separate_orders, +) logger = logging.getLogger(__name__) @@ -45,6 +49,7 @@ class WriteOutput(Role): learning_mode (bool, optional): Indicates if the simulation is in learning mode. Defaults to False. perform_evaluation (bool, optional): Indicates if the simulation is in evaluation mode. Defaults to False. additional_kpis (dict[str, OutputDef], optional): makes it possible to define additional kpis evaluated + max_dfs_size_mb (int, optional): The maximum storage size for storing output data before saving it. Defaults to 250 MB. """ def __init__( @@ -58,6 +63,7 @@ def __init__( learning_mode: bool = False, perform_evaluation: bool = False, additional_kpis: dict[str, OutputDef] = {}, + max_dfs_size_mb: int = 250, ): super().__init__() @@ -94,6 +100,10 @@ def __init__( # construct all timeframe under which hourly values are written to excel and db self.start = start self.end = end + + self.max_dfs_size = max_dfs_size_mb * 1024 * 1024 + self.current_dfs_size = 0 + # initializes dfs for storing and writing asynchronous self.write_dfs: dict = defaultdict(list) self.locks = defaultdict(lambda: Lock()) @@ -210,30 +220,38 @@ def handle_output_message(self, content: dict, meta: MetaDict): content (dict): The content of the message. meta (MetaDict): The metadata associated with the message. """ + content_data = content.get("data") if content.get("type") == "store_order_book": - self.write_market_orders(content.get("data"), content.get("market_id")) + self.write_market_orders(content_data, content.get("market_id")) elif content.get("type") == "store_market_results": - self.write_market_results(content.get("data")) + self.write_market_results(content_data) elif content.get("type") == "store_units": - self.write_units_definition(content.get("data")) + self.write_units_definition(content_data) elif content.get("type") == "market_dispatch": - self.write_market_dispatch(content.get("data")) + self.write_market_dispatch(content_data) elif content.get("type") == "unit_dispatch": - self.write_unit_dispatch(content.get("data")) + self.write_unit_dispatch(content_data) elif content.get("type") == "rl_learning_params": - self.write_rl_params(content.get("data")) + self.write_rl_params(content_data) elif content.get("type") == "grid_topology": - self.store_grid(content.get("data"), content.get("market_id")) + self.store_grid(content_data, content.get("market_id")) elif content.get("type") == "store_flows": - self.write_flows(content.get("data")) + self.write_flows(content_data) + + # # keep track of the memory usage of the data + self.current_dfs_size += calculate_content_size(content_data) + # if the current size is larger than self.max_dfs_size, store the data + if self.current_dfs_size > self.max_dfs_size: + logger.debug("storing output data due to size limit") + self.context.schedule_instant_task(coroutine=self.store_dfs()) def write_rl_params(self, rl_params: dict): """ @@ -311,6 +329,8 @@ async def store_dfs(self): self.write_dfs[table] = [] + self.current_dfs_size = 0 + def store_grid( self, grid: dict[str, pd.DataFrame], @@ -483,13 +503,15 @@ def write_market_dispatch(self, data: any): df["simulation"] = self.simulation_id self.write_dfs["market_dispatch"].append(df) - def write_unit_dispatch(self, data: any): + def write_unit_dispatch(self, unit_dispatch: dict): """ Writes the actual dispatch of the units to a CSV and database. Args: data (any): The records to be put into the table. Formatted like, "datetime, power, market_id, unit_id". """ + data = pd.concat([pd.DataFrame.from_dict(d) for d in unit_dispatch]) + data = data.set_index("time") data["simulation"] = self.simulation_id self.write_dfs["unit_dispatch"].append(data) diff --git a/assume/common/units_operator.py b/assume/common/units_operator.py index 6bf343fa8..e85b421e8 100644 --- a/assume/common/units_operator.py +++ b/assume/common/units_operator.py @@ -8,7 +8,6 @@ from itertools import groupby from operator import itemgetter -import pandas as pd from mango import Role, create_acl, sender_addr from mango.messages.message import Performatives @@ -254,7 +253,9 @@ def handle_data_request(self, content: DataRequestMessage, meta: MetaDict) -> No data = [] try: - data = self.units[unit].outputs[metric_type][start:end] + data = ( + self.units[unit].outputs[metric_type].as_pd_series(start=start, end=end) + ) except Exception: logger.exception("error handling data request") self.context.schedule_instant_message( @@ -292,7 +293,7 @@ def set_unit_dispatch( def get_actual_dispatch( self, product_type: str, last: datetime - ) -> tuple[pd.DataFrame, list[pd.DataFrame]]: + ) -> tuple[list[tuple[datetime, float, str, str]], list[dict]]: """ Retrieves the actual dispatch and commits it in the unit. We calculate the series of the actual market results dataframe with accepted bids. @@ -300,10 +301,10 @@ def get_actual_dispatch( Args: product_type (str): The product type for which this is done - last (datetime): the last date until which the dispatch was already sent + last (datetime.datetime): the last date until which the dispatch was already sent Returns: - tuple[pd.DataFrame, list[pd.DataFrame]]: market_dispatch and unit_dispatch dataframes + tuple[list[tuple[datetime, float, str, str]], list[dict]]: market_dispatch and unit_dispatch dataframes """ now = timestamp2datetime(self.context.current_timestamp) start = timestamp2datetime(last + 1) @@ -315,26 +316,28 @@ def get_actual_dispatch( groupby=["market_id", "unit_id"], ) - unit_dispatch_dfs = [] + unit_dispatch = [] for unit_id, unit in self.units.items(): current_dispatch = unit.execute_current_dispatch(start, now) end = now - current_dispatch.name = "power" - data = pd.DataFrame(current_dispatch) - - # TODO: this needs to be fixed. For now it is consuming too much time and is deactivated - # unit.calculate_generation_cost(start, now, "energy") - valid_outputs = ["soc", "cashflow", "marginal_costs", "total_costs"] + dispatch = {"power": current_dispatch} + unit.calculate_generation_cost(start, now, "energy") + valid_outputs = [ + "soc", + "cashflow", + "marginal_costs", + "total_costs", + ] for key in unit.outputs.keys(): for output in valid_outputs: if output in key: - data[key] = unit.outputs[key][start:end] + dispatch[key] = unit.outputs[key].loc[start:end] + dispatch["time"] = unit.index.get_date_list(start, end) + dispatch["unit"] = unit_id + unit_dispatch.append(dispatch) - data["unit"] = unit_id - unit_dispatch_dfs.append(data) - - return market_dispatch, unit_dispatch_dfs + return market_dispatch, unit_dispatch def write_actual_dispatch(self, product_type: str) -> None: """ @@ -350,9 +353,7 @@ def write_actual_dispatch(self, product_type: str) -> None: return self.last_sent_dispatch[product_type] = self.context.current_timestamp - market_dispatch, unit_dispatch_dfs = self.get_actual_dispatch( - product_type, last - ) + market_dispatch, unit_dispatch = self.get_actual_dispatch(product_type, last) now = timestamp2datetime(self.context.current_timestamp) self.valid_orders[product_type] = list( @@ -372,8 +373,7 @@ def write_actual_dispatch(self, product_type: str) -> None: "data": market_dispatch, }, ) - if unit_dispatch_dfs: - unit_dispatch = pd.concat(unit_dispatch_dfs) + if unit_dispatch: self.context.schedule_instant_message( receiver_addr=db_addr, content={ diff --git a/assume/common/utils.py b/assume/common/utils.py index 0c2248715..165129b23 100644 --- a/assume/common/utils.py +++ b/assume/common/utils.py @@ -333,31 +333,6 @@ def aggregate_step_amount(orderbook: Orderbook, begin=None, end=None, groupby=No return [j for sub in list(aggregation.values()) for j in sub] -def get_test_demand_orders(power: np.ndarray): - """ - Get test demand orders. - - Args: - power (numpy.ndarray): Power array. - - Returns: - pandas.DataFrame: DataFrame of demand orders. - - Examples: - >>> power = np.array([100, 200, 150]) - >>> get_test_demand_orders(power) - """ - - order_book = {} - for t in range(len(power)): - order_book[t] = dict( - type="demand", hour=t, block_id=t, name="DEM", price=3, volume=-power[t] - ) - demand_order = pd.DataFrame.from_dict(order_book, orient="index") - demand_order = demand_order.set_index(["block_id", "hour", "name"]) - return demand_order - - def separate_orders(orderbook: Orderbook): """ Separate orders with several hours into single hour orders. @@ -674,3 +649,36 @@ def suppress_output(): os.close(saved_stdout_fd) os.close(saved_stderr_fd) os.close(devnull) + + +# Function to parse the duration string +def parse_duration(duration_str): + if duration_str.endswith("d"): + days = float(duration_str[:-1]) + return timedelta(days=days) + elif duration_str.endswith("h"): + hours = float(duration_str[:-1]) + return timedelta(hours=hours) + elif duration_str.endswith("m"): + minutes = float(duration_str[:-1]) + return timedelta(minutes=minutes) + elif duration_str.endswith("s"): + seconds = float(duration_str[:-1]) + return timedelta(seconds=seconds) + else: + raise ValueError(f"Unsupported duration format: {duration_str}") + + +def calculate_content_size(content: list | dict) -> int: + """ + Calculate the size of a content in bytes. + """ + if isinstance(content, dict): # For dictionaries + return sys.getsizeof(content) + sum( + sys.getsizeof(value) for value in content.values() + ) + elif isinstance(content, list): # For lists, including lists of dicts + return sys.getsizeof(content) + sum( + calculate_content_size(item) for item in content + ) + return sys.getsizeof(content) diff --git a/assume/markets/base_market.py b/assume/markets/base_market.py index 6264a53af..6f6eaf8d3 100644 --- a/assume/markets/base_market.py +++ b/assume/markets/base_market.py @@ -536,7 +536,7 @@ def handle_data_request(self, content: DataRequestMessage, meta: MetaDict): data = pd.DataFrame(self.results) data.index = data["time"] - data = data[metric_type][start:end] + data = data[metric_type].loc[start:end] except Exception: logger.exception("Error handling data request") @@ -683,7 +683,7 @@ async def clear_market(self, market_products: list[MarketProduct]): await self.store_market_results(market_meta) - if flows and len(flows) > 0: + if flows is not None and len(flows) > 0: await self.store_flows(flows) return accepted_orderbook, market_meta diff --git a/assume/markets/clearing_algorithms/complex_clearing.py b/assume/markets/clearing_algorithms/complex_clearing.py index 4524ca40d..ad938c624 100644 --- a/assume/markets/clearing_algorithms/complex_clearing.py +++ b/assume/markets/clearing_algorithms/complex_clearing.py @@ -243,7 +243,10 @@ def energy_balance_rule(model, node, t): for bid_id in instance.Bids: # Fix the binary variable to its value - instance.x[bid_id].fix(instance.x[bid_id].value) + value = instance.x[bid_id].value + if value is not None: + value = 1 if value >= 0.99 else 0 + instance.x[bid_id].fix(value) # Change the domain to Reals (or appropriate continuous domain) instance.x[bid_id].domain = pyo.Reals diff --git a/assume/markets/clearing_algorithms/contracts.py b/assume/markets/clearing_algorithms/contracts.py index 2e5ea841f..c5f755b5c 100644 --- a/assume/markets/clearing_algorithms/contracts.py +++ b/assume/markets/clearing_algorithms/contracts.py @@ -401,7 +401,7 @@ def ppa( tuple[dict, dict]: the buyer order and the seller order as a tuple """ buyer_agent, seller_agent = contract["contractor_id"], contract["agent_addr"] - volume = sum(future_generation_series[start:end]) + volume = sum(future_generation_series.loc[start:end]) buyer: Orderbook = [ { "bid_id": contract["contractor_unit_id"], @@ -461,7 +461,7 @@ def swingcontract( outer_price = contract["price"] * 1.5 # ct/kwh # TODO does not work with multiple markets with differing time scales.. # this only works for whole trading hours (as x MW*1h == x MWh) - demand = -demand_series[start:end] + demand = -demand_series.loc[start:end] normal = demand[minDCQ < demand and demand < maxDCQ] * set_price expensive = ~demand[minDCQ < demand and demand < maxDCQ] * outer_price price = sum(normal) + sum(expensive) @@ -522,13 +522,12 @@ def cfd( # TODO does not work with multiple markets with differing time scales.. # this only works for whole trading hours (as x MW*1h == x MWh) - # price_series = (contract["price"] - market_index[start:end]) * gen_series[seller][ - # start:end - # ] - price_series = (market_index[start:end] - contract["price"]) * gen_series[start:end] + price_series = (market_index.loc[start:end] - contract["price"]) * gen_series.loc[ + start:end + ] price_series = price_series.dropna() price = sum(price_series) - volume = sum(gen_series[start:end]) + volume = sum(gen_series.loc[start:end]) # volume is hard to calculate with differing units? # unit conversion is quite hard regarding the different intervals buyer: Orderbook = [ @@ -586,11 +585,13 @@ def market_premium( buyer_agent, seller_agent = contract["contractor_id"], contract["agent_addr"] # TODO does not work with multiple markets with differing time scales.. # this only works for whole trading hours (as x MW*1h == x MWh) - price_series = (market_index[start:end] - contract["price"]) * gen_series[start:end] + price_series = (market_index.loc[start:end] - contract["price"]) * gen_series.loc[ + start:end + ] price_series = price_series.dropna() # sum only where market price is below contract price price = sum(price_series[price_series < 0]) - volume = sum(gen_series[start:end]) + volume = sum(gen_series.loc[start:end]) # volume is hard to calculate with differing units? # unit conversion is quite hard regarding the different intervals buyer: Orderbook = [ @@ -634,7 +635,7 @@ def feed_in_tariff( buyer_agent, seller_agent = contract["contractor_id"], contract["agent_addr"] # TODO does not work with multiple markets with differing time scales.. # this only works for whole trading hours (as x MW*1h == x MWh) - price_series = contract["price"] * client_series[start:end] + price_series = contract["price"] * client_series.loc[start:end] price = sum(price_series) # volume is hard to calculate with differing units? # unit conversion is quite hard regarding the different intervals diff --git a/assume/reinforcement_learning/learning_unit_operator.py b/assume/reinforcement_learning/learning_unit_operator.py index 0ebe82837..97c71194d 100644 --- a/assume/reinforcement_learning/learning_unit_operator.py +++ b/assume/reinforcement_learning/learning_unit_operator.py @@ -140,14 +140,14 @@ def write_learning_to_output(self, orderbook: Orderbook, market_id: str) -> None else: output_dict.update( { - "profit": unit.outputs["profit"].loc[start], - "reward": unit.outputs["reward"].loc[start], - "regret": unit.outputs["regret"].loc[start], + "profit": unit.outputs["profit"].at[start], + "reward": unit.outputs["reward"].at[start], + "regret": unit.outputs["regret"].at[start], } ) - action_tuple = unit.outputs["actions"].loc[start] - noise_tuple = unit.outputs["exploration_noise"].loc[start] + action_tuple = unit.outputs["actions"].at[start] + noise_tuple = unit.outputs["exploration_noise"].at[start] action_dim = action_tuple.numel() for i in range(action_dim): diff --git a/assume/scenario/loader_amiris.py b/assume/scenario/loader_amiris.py index 9b022d3c8..3b4eb127f 100644 --- a/assume/scenario/loader_amiris.py +++ b/assume/scenario/loader_amiris.py @@ -135,6 +135,7 @@ def add_agent_to_world( base_path: str, markups: dict = {}, supports: dict = {}, + index: pd.DatetimeIndex = None, ): """ Adds an agent from a amiris agent definition to the ASSUME world. @@ -175,7 +176,7 @@ def add_agent_to_world( "technology": "demand", "price": value, }, - NaiveForecast(world.index, demand=100000), + NaiveForecast(index, demand=100000), ) case "EnergyExchange" | "DayAheadMarketSingleZone": clearing_section = agent["Attributes"].get("Clearing", agent["Attributes"]) @@ -223,7 +224,7 @@ def add_agent_to_world( co2_price = agent["Attributes"]["Co2Prices"] if isinstance(co2_price, str): price_series = read_csv(base_path, co2_price) - co2_price = price_series.reindex(world.index).ffill().fillna(0) + co2_price = price_series.reindex(index).ffill().fillna(0) prices["co2"] = co2_price case "FuelsMarket": # fill prices for forecaster @@ -235,7 +236,7 @@ def add_agent_to_world( price_series.index = price_series.index.round("h") if not price_series.index.is_unique: price_series = price_series.groupby(level=0).last() - price = price_series.reindex(world.index).ffill() + price = price_series.reindex(index).ffill() prices[fuel_type] = price * fuel["ConversionFactor"] case "DemandTrader": world.add_unit_operator(agent["Id"]) @@ -253,7 +254,7 @@ def add_agent_to_world( "technology": "demand", "price": load["ValueOfLostLoad"], }, - NaiveForecast(world.index, demand=demand_series), + NaiveForecast(index, demand=demand_series), ) case "StorageTrader": @@ -270,7 +271,7 @@ def add_agent_to_world( forecast_price = prices.get("co2", 20) # TODO forecast should be calculated using calculate_EOM_price_forecast forecast = NaiveForecast( - world.index, + index, availability=1, co2_price=prices.get("co2", 2), # price_forecast is used for price_EOM @@ -336,11 +337,11 @@ def add_agent_to_world( availability = prototype["PlannedAvailability"] if isinstance(availability, str): availability = read_csv(base_path, availability) - availability = availability.reindex(world.index).ffill() + availability = availability.reindex(index).ffill() availability *= prototype.get("UnplannedAvailabilityFactor", 1) forecast = NaiveForecast( - world.index, + index, availability=availability, fuel_price=fuel_price, co2_price=prices.get("co2", 2), @@ -387,7 +388,7 @@ def add_agent_to_world( max_power = attr["InstalledPowerInMW"] if isinstance(availability, str): dispatch_profile = read_csv(base_path, availability) - availability = dispatch_profile.reindex(world.index).ffill().fillna(0) + availability = dispatch_profile.reindex(index).ffill().fillna(0) if availability.max() > 1: scale_value = availability.max() @@ -398,7 +399,7 @@ def add_agent_to_world( fuel_price = prices.get(translate_fuel_type[attr["EnergyCarrier"]], 0) fuel_price += attr.get("OpexVarInEURperMWH", 0) forecast = NaiveForecast( - world.index, + index, availability=availability, fuel_price=fuel_price, co2_price=prices.get("co2", 0), @@ -486,7 +487,6 @@ def load_amiris( start=start, end=end, simulation_id=sim_id, - index=index, ) # helper dict to map trader markups/markdowns to powerplants markups = {} @@ -521,6 +521,7 @@ def load_amiris( base_path, markups, supports, + index, ) # calculate market price before simulation world diff --git a/assume/scenario/loader_csv.py b/assume/scenario/loader_csv.py index 75133f0a2..fcb36ca2b 100644 --- a/assume/scenario/loader_csv.py +++ b/assume/scenario/loader_csv.py @@ -514,13 +514,14 @@ def load_config_and_create_forecaster( forecaster.set_forecast(temperature_df) forecaster.calc_forecast_if_needed() + forecaster.convert_forecasts_to_fast_series() + return { "config": config, "sim_id": sim_id, "path": path, "start": start, "end": end, - "index": index, "powerplant_units": powerplant_units, "storage_units": storage_units, "demand_units": demand_units, @@ -563,13 +564,13 @@ def setup_world( config = scenario_data["config"] start = scenario_data["start"] end = scenario_data["end"] - index = scenario_data["index"] powerplant_units = scenario_data["powerplant_units"] storage_units = scenario_data["storage_units"] demand_units = scenario_data["demand_units"] dsm_units = scenario_data["dsm_units"] forecaster = scenario_data["forecaster"] + # save every thousand steps by default to free up memory save_frequency_hours = config.get("save_frequency_hours", 48) # Disable save frequency if CSV export is enabled if world.export_csv_path and save_frequency_hours is not None: @@ -625,7 +626,6 @@ def setup_world( simulation_id=sim_id, learning_config=learning_config, bidding_params=bidding_strategy_params, - index=index, forecaster=forecaster, ) diff --git a/assume/scenario/loader_oeds.py b/assume/scenario/loader_oeds.py index 03de25f1b..5cecd78a9 100644 --- a/assume/scenario/loader_oeds.py +++ b/assume/scenario/loader_oeds.py @@ -61,7 +61,6 @@ def load_oeds( end=end, save_frequency_hours=48, simulation_id=sim_id, - index=index, ) # setup eom market diff --git a/assume/scenario/loader_pypsa.py b/assume/scenario/loader_pypsa.py index 8fc6e5e74..1321f6944 100644 --- a/assume/scenario/loader_pypsa.py +++ b/assume/scenario/loader_pypsa.py @@ -38,7 +38,7 @@ def load_pypsa( marketdesign (list[MarketConfig]): description of the market design which will be used with the scenario """ index = network.snapshots - index.freq = "h" + index.freq = index.inferred_freq start = index[0] end = index[-1] sim_id = f"{scenario}_{study_case}" @@ -49,7 +49,6 @@ def load_pypsa( end=end, save_frequency_hours=save_frequency_hours, simulation_id=sim_id, - index=index, ) # setup eom market diff --git a/assume/strategies/advanced_orders.py b/assume/strategies/advanced_orders.py index 38945df1f..ad740ae48 100644 --- a/assume/strategies/advanced_orders.py +++ b/assume/strategies/advanced_orders.py @@ -2,10 +2,10 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later -import pandas as pd from assume.common.base import BaseStrategy, SupportsMinMax from assume.common.market_objects import MarketConfig, Orderbook, Product +from assume.common.utils import parse_duration from assume.strategies.flexable import ( calculate_EOM_price_if_off, calculate_EOM_price_if_on, @@ -29,7 +29,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # check if kwargs contains eom_foresight argument - self.foresight = pd.Timedelta(kwargs.get("eom_foresight", "12h")) + self.foresight = parse_duration(kwargs.get("eom_foresight", "12h")) def calculate_bids( self, @@ -63,7 +63,7 @@ def calculate_bids( end = product_tuples[-1][1] previous_power = unit.get_output_before(start) - min_power, max_power = unit.calculate_min_max_power(start, end) + min_power_values, max_power_values = unit.calculate_min_max_power(start, end) bids = [] bid_quantity_block = {} @@ -71,7 +71,9 @@ def calculate_bids( op_time = unit.get_operation_time(start) avg_op_time, avg_down_time = unit.get_average_operation_times(start) - for product in product_tuples: + for product, min_power, max_power in zip( + product_tuples, min_power_values, max_power_values + ): start = product[0] end = product[1] @@ -86,15 +88,15 @@ def calculate_bids( # ============================================================================= # adjust max_power for ramp speed - max_power[start] = unit.calculate_ramp( - op_time, previous_power, max_power[start], current_power + max_power = unit.calculate_ramp( + op_time, previous_power, max_power, current_power ) # adjust min_power for ramp speed - min_power[start] = unit.calculate_ramp( - op_time, previous_power, min_power[start], current_power + min_power = unit.calculate_ramp( + op_time, previous_power, min_power, current_power ) - bid_quantity_inflex = min_power[start] + bid_quantity_inflex = min_power # ============================================================================= # Calculating marginal cost @@ -104,7 +106,7 @@ def calculate_bids( start, current_power + bid_quantity_inflex ) marginal_cost_flex = unit.calculate_marginal_cost( - start, current_power + max_power[start] + start, current_power + max_power ) # ============================================================================= @@ -129,16 +131,17 @@ def calculate_bids( avg_op_time=avg_op_time, ) - if unit.outputs["heat"][start] > 0: + if unit.outputs["heat"].at[start] > 0: power_loss_ratio = ( - unit.outputs["power_loss"][start] / unit.outputs["heat"][start] + unit.outputs["power_loss"].at[start] + / unit.outputs["heat"].at[start] ) else: power_loss_ratio = 0.0 # Flex-bid price formulation if op_time <= -unit.min_down_time or op_time > 0: - bid_quantity_flex = max_power[start] - bid_quantity_inflex + bid_quantity_flex = max_power - bid_quantity_inflex bid_price_flex = (1 - power_loss_ratio) * marginal_cost_flex # add volume and price to block bid @@ -219,7 +222,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # check if kwargs contains eom_foresight argument - self.foresight = pd.Timedelta(kwargs.get("eom_foresight", "12h")) + self.foresight = parse_duration(kwargs.get("eom_foresight", "12h")) def calculate_bids( self, @@ -252,7 +255,7 @@ def calculate_bids( end = product_tuples[-1][1] previous_power = unit.get_output_before(start) - min_power, max_power = unit.calculate_min_max_power(start, end) + min_power_values, max_power_values = unit.calculate_min_max_power(start, end) bids = [] bid_quantity_block = {} @@ -262,7 +265,9 @@ def calculate_bids( block_id = unit.id + "_block" - for product in product_tuples: + for product, min_power, max_power in zip( + product_tuples, min_power_values, max_power_values + ): start = product[0] end = product[1] @@ -277,15 +282,15 @@ def calculate_bids( # ============================================================================= # adjust max_power for ramp speed - max_power[start] = unit.calculate_ramp( - op_time, previous_power, max_power[start], current_power + max_power = unit.calculate_ramp( + op_time, previous_power, max_power, current_power ) # adjust min_power for ramp speed - min_power[start] = unit.calculate_ramp( - op_time, previous_power, min_power[start], current_power + min_power = unit.calculate_ramp( + op_time, previous_power, min_power, current_power ) - bid_quantity_inflex = min_power[start] + bid_quantity_inflex = min_power # ============================================================================= # Calculating marginal cost @@ -295,7 +300,7 @@ def calculate_bids( start, current_power + bid_quantity_inflex ) marginal_cost_flex = unit.calculate_marginal_cost( - start, current_power + max_power[start] + start, current_power + max_power ) # ============================================================================= @@ -320,16 +325,17 @@ def calculate_bids( avg_op_time=avg_op_time, ) - if unit.outputs["heat"][start] > 0: + if unit.outputs["heat"].at[start] > 0: power_loss_ratio = ( - unit.outputs["power_loss"][start] / unit.outputs["heat"][start] + unit.outputs["power_loss"].at[start] + / unit.outputs["heat"].at[start] ) else: power_loss_ratio = 0.0 # Flex-bid price formulation if op_time <= -unit.min_down_time or op_time > 0: - bid_quantity_flex = max_power[start] - bid_quantity_inflex + bid_quantity_flex = max_power - bid_quantity_inflex bid_price_flex = (1 - power_loss_ratio) * marginal_cost_flex bid_quantity_block[product[0]] = bid_quantity_inflex diff --git a/assume/strategies/dmas_powerplant.py b/assume/strategies/dmas_powerplant.py index e8eea238e..0647c5af5 100644 --- a/assume/strategies/dmas_powerplant.py +++ b/assume/strategies/dmas_powerplant.py @@ -202,19 +202,19 @@ def build_model( # -> fuel costs fuel_cost = [ - (self.model.p_out[t] / unit.efficiency) * fuel_prices.iloc[t] for t in tr + (self.model.p_out[t] / unit.efficiency) * fuel_prices[t] for t in tr ] # -> emission costs emission_cost = [ (self.model.p_out[t] / unit.efficiency * unit.emission_factor) - * emission_prices.iloc[t] + * emission_prices[t] for t in tr ] # -> start costs start_cost = [self.model.v[t] * unit.cold_start_cost for t in tr] # -> profit and resulting cashflow - profit = [self.model.p_out[t] * power_prices.iloc[t] for t in tr] + profit = [self.model.p_out[t] * power_prices[t] for t in tr] cashflow = [ profit[t] - (fuel_cost[t] + emission_cost[t] + start_cost[t]) for t in tr ] @@ -308,8 +308,8 @@ def optimize( ) hour_count2 = 2 * hour_count steps = steps or self.steps - prices_24h = prices.iloc[:hour_count].copy() - prices_48h = prices.iloc[:hour_count2].copy() + prices_24h = prices[:hour_count].copy() + prices_48h = prices[:hour_count2].copy() try: fuel_prices = unit.forecaster.get_price(unit.fuel_type) emission_prices = unit.forecaster.get_price("co2") @@ -318,9 +318,14 @@ def optimize( raise Exception(f"No Fuel prices given for fuel {unit.fuel_type}") for step in steps: - adjusted_price = base_price.iloc[:hour_count] + step + adjusted_price = base_price[:hour_count] + step cashflow = self.build_model( - unit, start, hour_count, emission_prices, fuel_prices, adjusted_price + unit, + start, + hour_count, + emission_prices.iloc[:hour_count], + fuel_prices.iloc[:hour_count], + adjusted_price, ) self.model.obj = Objective(expr=quicksum(cashflow), sense=maximize) r = self.opt.solve(self.model) @@ -331,8 +336,8 @@ def optimize( self._set_results( unit, - emission_prices[:hour_count], - fuel_prices[:hour_count], + emission_prices.iloc[:hour_count], + fuel_prices.iloc[:hour_count], adjusted_price, start=start, step=step, @@ -354,8 +359,8 @@ def optimize( unit, start, hour_count, - emission_prices[:hour_count], - fuel_prices[:hour_count], + emission_prices.iloc[:hour_count], + fuel_prices.iloc[:hour_count], prices_24h, runtime, p0, @@ -377,8 +382,8 @@ def optimize( unit, start, hour_count2, - emission_prices[:hour_count2], - fuel_prices[:hour_count2], + emission_prices.iloc[:hour_count2], + fuel_prices.iloc[:hour_count2], prices_48h, runtime, p0, @@ -413,7 +418,7 @@ def optimize( for key in ["power", "emission", "fuel", "start", "profit"]: self.opt_results[step][key] = np.zeros(self.T) self.opt_results[step]["obj"] = 0 - return unit.outputs["generation"][start:] + return unit.outputs["generation"].loc[start:] def calculate_bids( self, @@ -457,8 +462,8 @@ def calculate_bids( self.optimize(unit, start, hour_count, base_price) def get_cost(p: float, t: int): - f = fuel_price.iloc[t] - e = e_price.iloc[t] + f = fuel_price[t] + e = e_price[t] return (p / unit.efficiency) * (f + e * unit.emission_factor) def get_marginal(p0: float, p1: float, t: int): diff --git a/assume/strategies/dmas_storage.py b/assume/strategies/dmas_storage.py index e08d58c2f..2a98d3eb3 100644 --- a/assume/strategies/dmas_storage.py +++ b/assume/strategies/dmas_storage.py @@ -184,7 +184,7 @@ def optimize( ] for key, func in PRICE_FUNCS.items(): - prices = func(base_price.values) + prices = func(base_price) self.power = self.build_model(unit, start, hour_count) profit = [-self.power[t] * prices[t] for t in time_range] self.model.obj = pyo.Objective( @@ -256,14 +256,14 @@ def calculate_bids( bid_hours = np.argwhere(power < 0).flatten() ask_hours = np.argwhere(power > 0).flatten() if len(bid_hours) > 1: - max_charging_price = power_prices.values[bid_hours].max() + max_charging_price = power_prices[bid_hours].max() else: max_charging_price = 0 min_discharging_price = max_charging_price / ( unit.efficiency_discharge * unit.efficiency_discharge ) - prc[ask_hours] = (power_prices.iloc[ask_hours] + min_discharging_price) / 2 - prc[bid_hours] = power_prices.values[bid_hours] + prc[ask_hours] = (power_prices[ask_hours] + min_discharging_price) / 2 + prc[bid_hours] = power_prices[bid_hours] add = True for orders in total_orders.values(): if any(prc != orders["price"]) or any(power != orders["volume"]): diff --git a/assume/strategies/extended.py b/assume/strategies/extended.py index 60eab76ec..5f644cdd7 100644 --- a/assume/strategies/extended.py +++ b/assume/strategies/extended.py @@ -44,7 +44,7 @@ def calculate_bids( start = product[0] end = product[1] - min_power, max_power = unit.calculate_min_max_power( + _, max_power = unit.calculate_min_max_power( start, end ) # max_power describes the maximum power output of the unit current_power = unit.outputs[ @@ -52,7 +52,7 @@ def calculate_bids( ].at[ start ] # current power output describes the power output at the start of the product - volume = max_power[start] + volume = max_power[0] if "OTC" in market_config.market_id: volume *= self.scale price = unit.calculate_marginal_cost(start, current_power + volume) @@ -208,15 +208,14 @@ def calculate_bids( start = product[0] end = product[1] - min_power, max_power = unit.calculate_min_max_power( - start, end - ) # max_power describes the maximum power output of the unit + # max_power describes the maximum power output of the unit + _, max_power = unit.calculate_min_max_power(start, end) current_power = unit.outputs[ "energy" ].at[ start ] # current power output describes the power output at the start of the product - volume = max_power[start] + volume = max_power[0] price = unit.calculate_marginal_cost(start, current_power + volume) bids.append( diff --git a/assume/strategies/flexable.py b/assume/strategies/flexable.py index 3a3d86642..2d0b8494d 100644 --- a/assume/strategies/flexable.py +++ b/assume/strategies/flexable.py @@ -4,11 +4,11 @@ from datetime import datetime, timedelta -import pandas as pd +import numpy as np from assume.common.base import BaseStrategy, SupportsMinMax from assume.common.market_objects import MarketConfig, Orderbook, Product -from assume.common.utils import get_products_index +from assume.common.utils import get_products_index, parse_duration class flexableEOM(BaseStrategy): @@ -27,7 +27,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # check if kwargs contains eom_foresight argument - self.foresight = pd.Timedelta(kwargs.get("eom_foresight", "12h")) + self.foresight = parse_duration(kwargs.get("eom_foresight", "12h")) def calculate_bids( self, @@ -60,13 +60,15 @@ def calculate_bids( end = product_tuples[-1][1] previous_power = unit.get_output_before(start) - min_power, max_power = unit.calculate_min_max_power(start, end) + min_power_values, max_power_values = unit.calculate_min_max_power(start, end) - bids = [] op_time = unit.get_operation_time(start) avg_op_time, avg_down_time = unit.get_average_operation_times(start) - for product in product_tuples: + bids = [] + for product, min_power, max_power in zip( + product_tuples, min_power_values, max_power_values + ): bid_quantity_inflex, bid_price_inflex = 0, 0 bid_quantity_flex, bid_price_flex = 0, 0 @@ -81,21 +83,21 @@ def calculate_bids( current_power = unit.outputs["energy"].at[start] # adjust max_power for ramp speed - max_power[start] = unit.calculate_ramp( - op_time, previous_power, max_power[start], current_power + max_power = unit.calculate_ramp( + op_time, previous_power, max_power, current_power ) # adjust min_power for ramp speed - min_power[start] = unit.calculate_ramp( - op_time, previous_power, min_power[start], current_power + min_power = unit.calculate_ramp( + op_time, previous_power, min_power, current_power ) - bid_quantity_inflex = min_power[start] + bid_quantity_inflex = min_power marginal_cost_inflex = unit.calculate_marginal_cost( start, current_power + bid_quantity_inflex ) marginal_cost_flex = unit.calculate_marginal_cost( - start, current_power + max_power[start] + start, current_power + max_power ) # ============================================================================= @@ -120,16 +122,17 @@ def calculate_bids( avg_op_time=avg_op_time, ) - if unit.outputs["heat"][start] > 0: + if unit.outputs["heat"].at[start] > 0: power_loss_ratio = ( - unit.outputs["power_loss"][start] / unit.outputs["heat"][start] + unit.outputs["power_loss"].at[start] + / unit.outputs["heat"].at[start] ) else: power_loss_ratio = 0.0 # Flex-bid price formulation if op_time <= -unit.min_down_time or op_time > 0: - bid_quantity_flex = max_power[start] - bid_quantity_inflex + bid_quantity_flex = max_power - bid_quantity_inflex bid_price_flex = (1 - power_loss_ratio) * marginal_cost_flex bids.append( @@ -199,7 +202,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # check if kwargs contains crm_foresight argument - self.foresight = pd.Timedelta(kwargs.get("crm_foresight", "4h")) + self.foresight = parse_duration(kwargs.get("crm_foresight", "4h")) def calculate_bids( self, @@ -228,12 +231,12 @@ def calculate_bids( start = product_tuples[0][0] end = product_tuples[-1][1] previous_power = unit.get_output_before(start) - min_power, max_power = unit.calculate_min_max_power( + _, max_power_values = unit.calculate_min_max_power( start, end, market_config.product_type ) # get max_power for the product type bids = [] - for product in product_tuples: + for product, max_power in zip(product_tuples, max_power_values): start = product[0] op_time = unit.get_operation_time(start) @@ -241,7 +244,7 @@ def calculate_bids( current_power = unit.outputs["energy"].at[start] # max_power + current_power < previous_power + unit.ramp_up bid_quantity = unit.calculate_ramp( - op_time, previous_power, max_power[start], current_power + op_time, previous_power, max_power, current_power ) if bid_quantity == 0: @@ -274,6 +277,13 @@ def calculate_bids( raise ValueError( f"Product {market_config.product_type} is not supported by this strategy." ) + + # clip price by max and min bid price defined by the MarketConfig + if price >= 0: + price = min(price, market_config.maximum_bid_price) + else: + price = max(price, market_config.minimum_bid_price) + bids.append( { "start_time": start, @@ -307,7 +317,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # check if kwargs contains crm_foresight argument - self.foresight = pd.Timedelta(kwargs.get("crm_foresight", "4h")) + self.foresight = parse_duration(kwargs.get("crm_foresight", "4h")) def calculate_bids( self, @@ -334,19 +344,19 @@ def calculate_bids( start = product_tuples[0][0] end = product_tuples[-1][1] previous_power = unit.get_output_before(start) - min_power, max_power = unit.calculate_min_max_power(start, end) + min_power_values, _ = unit.calculate_min_max_power(start, end) bids = [] - for product in product_tuples: + for product, min_power in zip(product_tuples, min_power_values): start = product[0] op_time = unit.get_operation_time(start) current_power = unit.outputs["energy"].at[start] # min_power + current_power > previous_power - unit.ramp_down - min_power[start] = unit.calculate_ramp( - op_time, previous_power, min_power[start], current_power + min_power = unit.calculate_ramp( + op_time, previous_power, min_power, current_power ) - bid_quantity = previous_power - min_power[start] + bid_quantity = previous_power - min_power if bid_quantity <= 0: continue @@ -382,6 +392,13 @@ def calculate_bids( raise ValueError( f"Product {market_config.product_type} is not supported by this strategy." ) + + # clip price by max and min bid price defined by the MarketConfig + if price >= 0: + price = min(price, market_config.maximum_bid_price) + else: + price = max(price, market_config.minimum_bid_price) + bids.append( { "start_time": start, @@ -518,7 +535,7 @@ def get_specific_revenue( and marginal costs for the time defined by the foresight. Args: - price_forecast (pandas.Series): The price forecast. + price_forecast (FastSeries): The price forecast. marginal_cost (float): The marginal cost of the unit. t (datetime.datetime): The start time of the product. foresight (datetime.timedelta): The foresight of the unit. @@ -543,74 +560,89 @@ def calculate_reward_EOM( orderbook: Orderbook, ): """ - Calculates and writes reward (costs and profit) for EOM market. + Calculate and write reward, profit and regret to unit outputs. Args: - unit (SupportsMinMax): A unit that the unit operator manages. - marketconfig (MarketConfig): A market configuration. - orderbook (Orderbook): An orderbook with accepted and rejected orders for the unit. + unit (SupportsMinMax): The unit to calculate reward for. + marketconfig (MarketConfig): The market configuration. + orderbook (Orderbook): The Orderbook. + + Note: + The reward is calculated as the profit minus the opportunity cost, + which is the loss of income we have because we are not running at full power. + The regret is the opportunity cost. + Because the regret_scale is set to 0 the reward equals the profit. + The profit is the income we have from the accepted bids. + The total costs are the running costs and the start-up costs. + """ # TODO: Calculate profits over all markets product_type = marketconfig.product_type products_index = get_products_index(orderbook) - max_power = ( + max_power_values = ( unit.forecaster.get_availability(unit.id)[products_index] * unit.max_power ) - profit = pd.Series(0.0, index=products_index) - reward = pd.Series(0.0, index=products_index) - opportunity_cost = pd.Series(0.0, index=products_index) - costs = pd.Series(0.0, index=products_index) + # Initialize intermediate results as numpy arrays for better performance + profit = np.zeros(len(products_index)) + reward = np.zeros(len(products_index)) + opportunity_cost = np.zeros(len(products_index)) + costs = np.zeros(len(products_index)) + + # Map products_index to their positions for faster updates + index_map = {time: i for i, time in enumerate(products_index)} for order in orderbook: start = order["start_time"] - end = order["end_time"] - end_excl = end - unit.index.freq + end_excl = order["end_time"] - unit.index.freq - order_times = pd.date_range(start, end_excl, freq=unit.index.freq) + order_times = unit.index[start:end_excl] + accepted_volume = order["accepted_volume"] + accepted_price = order["accepted_price"] + + for start, max_power in zip(order_times, max_power_values): + idx = index_map.get(start) - for start in order_times: marginal_cost = unit.calculate_marginal_cost( - start, unit.outputs[product_type].loc[start] + start, unit.outputs[product_type].at[start] ) - if isinstance(order["accepted_volume"], dict): - accepted_volume = order["accepted_volume"][start] + if isinstance(accepted_volume, dict): + accepted_volume = accepted_volume.get(start, 0) else: - accepted_volume = order["accepted_volume"] + accepted_volume = accepted_volume - if isinstance(order["accepted_price"], dict): - accepted_price = order["accepted_price"][start] + if isinstance(accepted_price, dict): + accepted_price = accepted_price.get(start, 0) else: - accepted_price = order["accepted_price"] + accepted_price = accepted_price price_difference = accepted_price - marginal_cost # calculate opportunity cost # as the loss of income we have because we are not running at full power order_opportunity_cost = price_difference * ( - max_power[start] - unit.outputs[product_type].loc[start] + max_power - unit.outputs[product_type].at[start] ) # if our opportunity costs are negative, we did not miss an opportunity to earn money and we set them to 0 # don't consider opportunity_cost more than once! Always the same for one timestep and one market - opportunity_cost[start] = max(order_opportunity_cost, 0) - profit[start] += accepted_price * accepted_volume + opportunity_cost[idx] = max(order_opportunity_cost, 0) + profit[idx] += accepted_price * accepted_volume # consideration of start-up costs - for start in products_index: + for i, start in enumerate(products_index): op_time = unit.get_operation_time(start) - marginal_cost = unit.calculate_marginal_cost( - start, unit.outputs[product_type].loc[start] - ) - costs[start] += marginal_cost * unit.outputs[product_type].loc[start] + output = unit.outputs[product_type].at[start] + marginal_cost = unit.calculate_marginal_cost(start, output) + costs[i] += marginal_cost * output - if unit.outputs[product_type].loc[start] != 0 and op_time < 0: + if output != 0 and op_time < 0: start_up_cost = unit.get_starting_costs(op_time) - costs[start] += start_up_cost + costs[i] += start_up_cost - profit += -costs + profit -= costs scaling = 0.1 / unit.max_power regret_scale = 0.0 reward = (profit - regret_scale * opportunity_cost) * scaling @@ -620,3 +652,6 @@ def calculate_reward_EOM( unit.outputs["reward"].loc[products_index] = reward unit.outputs["regret"].loc[products_index] = opportunity_cost unit.outputs["total_costs"].loc[products_index] = costs + + if "rl_reward" in unit.outputs.keys(): + unit.outputs["rl_reward"].append(reward) diff --git a/assume/strategies/flexable_storage.py b/assume/strategies/flexable_storage.py index 897d8b908..8bb5a5901 100644 --- a/assume/strategies/flexable_storage.py +++ b/assume/strategies/flexable_storage.py @@ -5,10 +5,10 @@ from datetime import timedelta import numpy as np -import pandas as pd from assume.common.base import BaseStrategy, SupportsMinMaxCharge from assume.common.market_objects import MarketConfig, Orderbook, Product +from assume.common.utils import parse_duration class flexableEOMStorage(BaseStrategy): @@ -21,7 +21,7 @@ class flexableEOMStorage(BaseStrategy): Otherwise, the unit will charge with the price defined as the average price multiplied by the charge efficiency of the unit. Attributes: - foresight (pandas.Timedelta): Foresight for the average price calculation. + foresight (datetime.timedelta): Foresight for the average price calculation. Args: *args: Additional arguments. @@ -31,7 +31,7 @@ class flexableEOMStorage(BaseStrategy): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.foresight = pd.Timedelta(kwargs.get("eom_foresight", "12h")) + self.foresight = parse_duration(kwargs.get("eom_foresight", "12h")) def calculate_bids( self, @@ -66,63 +66,73 @@ def calculate_bids( previous_power = unit.get_output_before(start) # save a theoretic SOC to calculate the ramping - theoretic_SOC = unit.outputs["soc"][start] + theoretic_SOC = unit.outputs["soc"].at[start] # calculate min and max power for charging and discharging - min_power_charge, max_power_charge = unit.calculate_min_max_charge( - start, end_all + min_power_charge_values, max_power_charge_values = ( + unit.calculate_min_max_charge(start, end_all) ) - min_power_discharge, max_power_discharge = unit.calculate_min_max_discharge( - start, end_all + min_power_discharge_values, max_power_discharge_values = ( + unit.calculate_min_max_discharge(start, end_all) ) # ============================================================================= # Calculate bids # ============================================================================= bids = [] - for product in product_tuples: - start = product[0] - end = product[1] + + for ( + product, + max_power_discharge, + min_power_discharge, + max_power_charge, + min_power_charge, + ) in zip( + product_tuples, + max_power_discharge_values, + min_power_discharge_values, + max_power_charge_values, + min_power_charge_values, + ): + start, end = product[0], product[1] current_power = unit.outputs["energy"].at[start] current_power_discharge = max(current_power, 0) current_power_charge = min(current_power, 0) - # calculate ramping constraints - max_power_discharge[start] = unit.calculate_ramp_discharge( + # Calculate ramping constraints using helper function + max_power_discharge = unit.calculate_ramp_discharge( theoretic_SOC, previous_power, - max_power_discharge[start], + max_power_discharge, current_power_discharge, - min_power_discharge[start], + min_power_discharge, ) - min_power_discharge[start] = unit.calculate_ramp_discharge( + min_power_discharge = unit.calculate_ramp_discharge( theoretic_SOC, previous_power, - min_power_discharge[start], + min_power_discharge, current_power_discharge, - min_power_discharge[start], + min_power_discharge, ) - max_power_charge[start] = unit.calculate_ramp_charge( + max_power_charge = unit.calculate_ramp_charge( theoretic_SOC, previous_power, - max_power_charge[start], + max_power_charge, current_power_charge, - min_power_charge[start], + min_power_charge, ) - min_power_charge[start] = unit.calculate_ramp_charge( + min_power_charge = unit.calculate_ramp_charge( theoretic_SOC, previous_power, - min_power_charge[start], + min_power_charge, current_power_charge, - min_power_charge[start], + min_power_charge, ) - price_forecast = unit.forecaster[f"price_{market_config.market_id}"] # calculate average price average_price = calculate_price_average( - unit=unit, current_time=start, foresight=self.foresight, price_forecast=price_forecast, @@ -131,12 +141,12 @@ def calculate_bids( # if price is higher than average price, discharge # if price is lower than average price, charge # if price forecast favors discharge, but max discharge is zero, set a bid for charging - if price_forecast[start] >= average_price and max_power_discharge[start]: + if price_forecast[start] >= average_price and max_power_discharge: price = average_price / unit.efficiency_discharge - bid_quantity = max_power_discharge[start] + bid_quantity = max_power_discharge else: price = average_price * unit.efficiency_charge - bid_quantity = max_power_charge[start] + bid_quantity = max_power_charge bids.append( { @@ -191,23 +201,21 @@ def calculate_reward( for order in orderbook: start = order["start_time"] - end = order["end_time"] - end_excl = end - unit.index.freq - index = pd.date_range(start, end_excl, freq=unit.index.freq) - costs = pd.Series(0.0, index=index) - for start in index: - if unit.outputs[product_type][start] != 0: - costs[start] += abs( - unit.outputs[product_type][start] - * unit.calculate_marginal_cost( - start, unit.outputs[product_type][start] - ) - ) - - unit.outputs["profit"][index] = ( - unit.outputs[f"{product_type}_cashflow"][index] - costs + end_excl = order["end_time"] - unit.index.freq + + # Extract outputs and costs in one step + outputs = unit.outputs[product_type].loc[start:end_excl] + costs = np.where( + outputs != 0, + np.abs(outputs) + * np.array([unit.calculate_marginal_cost(start, x) for x in outputs]), + 0, ) - unit.outputs["total_costs"][index] = costs + + unit.outputs["profit"].loc[start:end_excl] = ( + unit.outputs[f"{product_type}_cashflow"].loc[start:end_excl] - costs + ) + unit.outputs["total_costs"].loc[start:end_excl] = costs class flexablePosCRMStorage(BaseStrategy): @@ -218,7 +226,7 @@ class flexablePosCRMStorage(BaseStrategy): Otherwise, the strategy bids the capacity_price for the capacity_pos product. Attributes: - foresight (pandas.Timedelta): Foresight for the average price calculation. + foresight (datetime.timedelta): Foresight for the average price calculation. Args: *args: Additional arguments. @@ -229,7 +237,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # check if kwargs contains crm_foresight argument - self.foresight = pd.Timedelta(kwargs.get("crm_foresight", "4h")) + self.foresight = parse_duration(kwargs.get("crm_foresight", "4h")) def calculate_bids( self, @@ -259,10 +267,13 @@ def calculate_bids( previous_power = unit.get_output_before(start) - _, max_power_discharge = unit.calculate_min_max_discharge(start, end) + _, max_power_discharge_values = unit.calculate_min_max_discharge(start, end) bids = [] - theoretic_SOC = unit.outputs["soc"][start] - for product in product_tuples: + theoretic_SOC = unit.outputs["soc"].at[start] + + for product, max_power_discharge in zip( + product_tuples, max_power_discharge_values + ): start = product[0] current_power = unit.outputs["energy"].at[start] @@ -270,7 +281,7 @@ def calculate_bids( bid_quantity = unit.calculate_ramp_discharge( theoretic_SOC, previous_power, - max_power_discharge[start], + max_power_discharge, current_power, ) @@ -348,7 +359,7 @@ class flexableNegCRMStorage(BaseStrategy): A strategy that bids the energy_price or the capacity_price of the unit on the negative CRM(reserve market). Attributes: - foresight (pandas.Timedelta): Foresight for the average price calculation. + foresight (datetime.timedelta): Foresight for the average price calculation. Args: *args: Additional arguments. @@ -358,7 +369,7 @@ class flexableNegCRMStorage(BaseStrategy): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.foresight = pd.Timedelta(kwargs.get("crm_foresight", "4h")) + self.foresight = parse_duration(kwargs.get("crm_foresight", "4h")) def calculate_bids( self, @@ -386,19 +397,19 @@ def calculate_bids( previous_power = unit.get_output_before(start) - theoretic_SOC = unit.outputs["soc"][start] + theoretic_SOC = unit.outputs["soc"].at[start] - _, max_power_charge = unit.calculate_min_max_charge(start, end) + _, max_power_charge_values = unit.calculate_min_max_charge(start, end) bids = [] - for product in product_tuples: + for product, max_power_charge in zip(product_tuples, max_power_charge_values): start = product[0] current_power = unit.outputs["energy"].at[start] bid_quantity = abs( unit.calculate_ramp_charge( theoretic_SOC, previous_power, - max_power_charge[start], + max_power_charge, current_power, ) ) @@ -450,22 +461,22 @@ def calculate_bids( return bids -def calculate_price_average(unit, current_time, foresight, price_forecast): +def calculate_price_average(current_time, foresight, price_forecast): """ Calculates the average price for a given foresight and returns the average price. Args: - unit (SupportsMinMaxCharge): The unit that is dispatched. - current_time (pandas.Timestamp): The current time. - foresight (pandas.Timedelta): The foresight. - price_forecast (pandas.Series): The price forecast. + current_time (datetime.datetime): The current time. + foresight (datetime.timedelta): The foresight. + price_forecast (FastSeries): The price forecast. Returns: float: The average price. """ - average_price = np.mean( - price_forecast[current_time - foresight : current_time + foresight] - ) + start = max(current_time - foresight, price_forecast.index[0]) + end = min(current_time + foresight, price_forecast.index[-1]) + + average_price = np.mean(price_forecast.loc[start:end]) return average_price @@ -479,34 +490,37 @@ def get_specific_revenue(unit, marginal_cost, t, foresight, price_forecast): unit (SupportsMinMaxCharge): The unit that is dispatched. marginal_cost (float): The marginal cost. t (datetime.datetime): The start time of the product. - foresight (pandas.Timedelta): The foresight. - price_forecast (pandas.Series): The price forecast. + foresight (datetime.timedelta): The foresight. + price_forecast (FastSeries): The price forecast. Returns: float: The specific revenue. """ if t + foresight > price_forecast.index[-1]: - price_forecast = price_forecast.loc[t:] - _, max_power_discharge = unit.calculate_min_max_discharge( + _, max_power_discharge_values = unit.calculate_min_max_discharge( start=t, end=price_forecast.index[-1] + unit.index.freq ) + price_forecast = price_forecast.loc[t:] else: - price_forecast = price_forecast.loc[t : t + foresight] - _, max_power_discharge = unit.calculate_min_max_discharge( + _, max_power_discharge_values = unit.calculate_min_max_discharge( start=t, end=t + foresight + unit.index.freq ) + price_forecast = price_forecast.loc[t : t + foresight] possible_revenue = 0 soc = unit.outputs["soc"][t] theoretic_SOC = soc previous_power = unit.get_output_before(t) - for i, market_price in enumerate(price_forecast): + + for market_price, max_power_discharge in zip( + price_forecast, max_power_discharge_values + ): theoretic_power_discharge = unit.calculate_ramp_discharge( theoretic_SOC, previous_power=previous_power, - power_discharge=max_power_discharge.iloc[i], + power_discharge=max_power_discharge, ) possible_revenue += (market_price - marginal_cost) * theoretic_power_discharge theoretic_SOC -= theoretic_power_discharge diff --git a/assume/strategies/learning_advanced_orders.py b/assume/strategies/learning_advanced_orders.py index 7c0d2ac99..a721dc53c 100644 --- a/assume/strategies/learning_advanced_orders.py +++ b/assume/strategies/learning_advanced_orders.py @@ -5,12 +5,11 @@ from datetime import datetime import numpy as np -import pandas as pd import torch as th from assume.common.base import SupportsMinMax from assume.common.market_objects import MarketConfig, Orderbook, Product -from assume.common.utils import get_products_index +from assume.strategies.flexable import calculate_reward_EOM from assume.strategies.learning_strategies import RLStrategy @@ -74,9 +73,6 @@ def calculate_bids( start = product_tuples[0][0] end = product_tuples[-1][1] - previous_power = unit.get_output_before(start) - min_power, max_power = unit.calculate_min_max_power(start, end) - # ============================================================================= # 1. Get the Observations, which are the basis of the action decision # ============================================================================= @@ -105,12 +101,17 @@ def calculate_bids( bid_price_inflex = min(bid_price_1, bid_price_2) bid_price_flex = max(bid_price_1, bid_price_2) + op_time = unit.get_operation_time(start) + + previous_power = unit.get_output_before(start) + min_power_values, max_power_values = unit.calculate_min_max_power(start, end) + # calculate the quantities and transform the bids into orderbook format bids = [] bid_quantity_block = {} - op_time = unit.get_operation_time(start) - - for product in product_tuples: + for product, min_power, max_power in zip( + product_tuples, min_power_values, max_power_values + ): start = product[0] end = product[1] @@ -121,22 +122,22 @@ def calculate_bids( # get technical bounds for the unit output from the unit # adjust for ramp speed - max_power[start] = unit.calculate_ramp( - op_time, previous_power, max_power[start], current_power + max_power = unit.calculate_ramp( + op_time, previous_power, max_power, current_power ) # adjust for ramp speed - min_power[start] = unit.calculate_ramp( - op_time, previous_power, min_power[start], current_power + min_power = unit.calculate_ramp( + op_time, previous_power, min_power, current_power ) # 3.1 formulate the bids for Pmin - bid_quantity_inflex = min_power[start] + bid_quantity_inflex = min_power # 3.1 formulate the bids for Pmax - Pmin # Pmin, the minimum run capacity is the inflexible part of the bid, which should always be accepted if op_time <= -unit.min_down_time or op_time > 0: - bid_quantity_flex = max_power[start] - bid_quantity_inflex + bid_quantity_flex = max_power - bid_quantity_inflex if "BB" in self.order_types: bid_quantity_block[start] = bid_quantity_inflex @@ -215,8 +216,8 @@ def calculate_bids( unit.outputs["rl_actions"].append(actions) # store results in unit outputs as series to be written to the database by the unit operator - unit.outputs["actions"][start] = actions - unit.outputs["exploration_noise"][start] = noise + unit.outputs["actions"].at[start] = actions + unit.outputs["exploration_noise"].at[start] = noise bids = self.remove_empty_bids(bids) @@ -257,7 +258,7 @@ def create_observation( end_excl = end - unit.index.freq # get the forecast length depending on the time unit considered in the modelled unit - forecast_len = pd.Timedelta((self.foresight - 1) * unit.index.freq) + forecast_len = (self.foresight - 1) * unit.index.freq # ============================================================================= # 1.1 Get the Observations, which are the basis of the action decision @@ -284,15 +285,15 @@ def create_observation( scaled_res_load_forecast = ( unit.forecaster[f"residual_load_{market_id}"][ -int(product_len + self.foresight - 1) : - ].values + ] / scaling_factor_res_load ) else: scaled_res_load_forecast = ( - unit.forecaster[f"residual_load_{market_id}"] - .loc[start : end_excl + forecast_len] - .values + unit.forecaster[f"residual_load_{market_id}"].loc[ + start : end_excl + forecast_len + ] / scaling_factor_res_load ) @@ -300,15 +301,15 @@ def create_observation( scaled_price_forecast = ( unit.forecaster[f"price_{market_id}"][ -int(product_len + self.foresight - 1) : - ].values + ] / scaling_factor_price ) else: scaled_price_forecast = ( - unit.forecaster[f"price_{market_id}"] - .loc[start : end_excl + forecast_len] - .values + unit.forecaster[f"price_{market_id}"].loc[ + start : end_excl + forecast_len + ] / scaling_factor_price ) @@ -375,89 +376,4 @@ def calculate_reward( """ - # ============================================================================= - # 4. Calculate Reward - # ============================================================================= - # function is called after the market is cleared and we get the market feedback, - # so we can calculate the profit - - product_type = marketconfig.product_type - products_index = get_products_index(orderbook) - - max_power = ( - unit.forecaster.get_availability(unit.id)[products_index] * unit.max_power - ) - - profit = pd.Series(0.0, index=products_index) - reward = pd.Series(0.0, index=products_index) - opportunity_cost = pd.Series(0.0, index=products_index) - costs = pd.Series(0.0, index=products_index) - - # iterate over all orders in the orderbook, to calculate order specific profit - for order in orderbook: - start = order["start_time"] - end = order["end_time"] - end_excl = end - unit.index.freq - - order_times = pd.date_range(start, end_excl, freq=unit.index.freq) - - # calculate profit as income - running_cost from this event - - for start in order_times: - marginal_cost = unit.calculate_marginal_cost( - start, unit.outputs[product_type].loc[start] - ) - if isinstance(order["accepted_volume"], dict): - accepted_volume = order["accepted_volume"][start] - else: - accepted_volume = order["accepted_volume"] - - if isinstance(order["accepted_price"], dict): - accepted_price = order["accepted_price"][start] - else: - accepted_price = order["accepted_price"] - - price_difference = accepted_price - marginal_cost - - # calculate opportunity cost - # as the loss of income we have because we are not running at full power - order_opportunity_cost = price_difference * ( - max_power[start] - unit.outputs[product_type].loc[start] - ) - # if our opportunity costs are negative, we did not miss an opportunity to earn money and we set them to 0 - # don't consider opportunity_cost more than once! Always the same for one timestep and one market - opportunity_cost[start] = max(order_opportunity_cost, 0) - profit[start] += accepted_price * accepted_volume - - # consideration of start-up costs, which are evenly divided between the - # upward and downward regulation events - for start in products_index: - op_time = unit.get_operation_time(start) - - marginal_cost = unit.calculate_marginal_cost( - start, unit.outputs[product_type].loc[start] - ) - costs[start] += marginal_cost * unit.outputs[product_type].loc[start] - - if unit.outputs[product_type].loc[start] != 0 and op_time < 0: - start_up_cost = unit.get_starting_costs(op_time) - costs[start] += start_up_cost - - # --------------------------- - # 4.1 Calculate Reward - # The straight forward implementation would be reward = profit, yet we would like to give the agent more guidance - # in the learning process, so we add a regret term to the reward, which is the opportunity cost - # define the reward and scale it - - profit += -costs - scaling = 1 / (unit.max_power * self.max_bid_price) - regret_scale = 0.0 - reward = (profit - regret_scale * opportunity_cost) * scaling - - # store results in unit outputs which are written to database by unit operator - unit.outputs["profit"].loc[products_index] = profit - unit.outputs["reward"].loc[products_index] = reward - unit.outputs["regret"].loc[products_index] = opportunity_cost - unit.outputs["total_costs"].loc[products_index] = costs - - unit.outputs["rl_rewards"].append(reward) + calculate_reward_EOM(unit, marketconfig, orderbook) diff --git a/assume/strategies/learning_strategies.py b/assume/strategies/learning_strategies.py index a8dc25777..4f9d949ba 100644 --- a/assume/strategies/learning_strategies.py +++ b/assume/strategies/learning_strategies.py @@ -7,7 +7,6 @@ from pathlib import Path import numpy as np -import pandas as pd import torch as th from assume.common.base import LearningStrategy, SupportsMinMax, SupportsMinMaxCharge @@ -227,8 +226,8 @@ def calculate_bids( end = product_tuples[0][1] # get technical bounds for the unit output from the unit min_power, max_power = unit.calculate_min_max_power(start, end) - min_power = min_power[start] - max_power = max_power[start] + min_power = min_power[0] + max_power = max_power[0] # ============================================================================= # 1. Get the Observations, which are the basis of the action decision @@ -287,8 +286,8 @@ def calculate_bids( unit.outputs["rl_actions"].append(actions) # store results in unit outputs as series to be written to the database by the unit operator - unit.outputs["actions"][start] = actions - unit.outputs["exploration_noise"][start] = noise + unit.outputs["actions"].at[start] = actions + unit.outputs["exploration_noise"].at[start] = noise return bids @@ -347,9 +346,10 @@ def get_actions(self, next_observation): curr_action += noise else: # if we are not in learning mode we just use the actor neural net to get the action without adding noise - curr_action = self.actor(next_observation).detach() - noise = tuple(0 for _ in range(self.act_dim)) + + # noise is an tensor with zeros, because we are not in learning mode + noise = th.zeros(self.act_dim, dtype=self.float_type) curr_action = curr_action.clamp(-1, 1) @@ -391,7 +391,7 @@ def create_observation( end_excl = end - unit.index.freq # get the forecast length depending on the tme unit considered in the modelled unit - forecast_len = pd.Timedelta((self.foresight - 1) * unit.index.freq) + forecast_len = (self.foresight - 1) * unit.index.freq # ============================================================================= # 1.1 Get the Observations, which are the basis of the action decision @@ -413,7 +413,7 @@ def create_observation( > unit.forecaster[f"residual_load_{market_id}"].index[-1] ): scaled_res_load_forecast = ( - unit.forecaster[f"residual_load_{market_id}"].loc[start:].values + unit.forecaster[f"residual_load_{market_id}"].loc[start:] / scaling_factor_res_load ) scaled_res_load_forecast = np.concatenate( @@ -427,16 +427,15 @@ def create_observation( else: scaled_res_load_forecast = ( - unit.forecaster[f"residual_load_{market_id}"] - .loc[start : end_excl + forecast_len] - .values + unit.forecaster[f"residual_load_{market_id}"].loc[ + start : end_excl + forecast_len + ] / scaling_factor_res_load ) if end_excl + forecast_len > unit.forecaster[f"price_{market_id}"].index[-1]: scaled_price_forecast = ( - unit.forecaster[f"price_{market_id}"].loc[start:].values - / scaling_factor_price + unit.forecaster[f"price_{market_id}"].loc[start:] / scaling_factor_price ) scaled_price_forecast = np.concatenate( [ @@ -449,9 +448,9 @@ def create_observation( else: scaled_price_forecast = ( - unit.forecaster[f"price_{market_id}"] - .loc[start : end_excl + forecast_len] - .values + unit.forecaster[f"price_{market_id}"].loc[ + start : end_excl + forecast_len + ] / scaling_factor_price ) @@ -526,7 +525,7 @@ def calculate_reward( # depending on way the unit calculates marginal costs we take costs marginal_cost = unit.calculate_marginal_cost( - start, unit.outputs[product_type].loc[start] + start, unit.outputs[product_type].at[start] ) duration = (end - start) / timedelta(hours=1) @@ -553,12 +552,12 @@ def calculate_reward( # consideration of start-up costs, which are evenly divided between the # upward and downward regulation events if ( - unit.outputs[product_type].loc[start] != 0 + unit.outputs[product_type].at[start] != 0 and unit.outputs[product_type].loc[start - unit.index.freq] == 0 ): costs += unit.hot_start_cost / 2 elif ( - unit.outputs[product_type].loc[start] == 0 + unit.outputs[product_type].at[start] == 0 and unit.outputs[product_type].loc[start - unit.index.freq] != 0 ): costs += unit.hot_start_cost / 2 @@ -776,8 +775,8 @@ def calculate_bids( _, max_discharge = unit.calculate_min_max_discharge(start, end_all) _, max_charge = unit.calculate_min_max_charge(start, end_all) - bid_quantity_supply = max_discharge.iloc[0] - bid_quantity_demand = max_charge.iloc[0] + bid_quantity_supply = max_discharge[0] + bid_quantity_demand = max_charge[0] bids = [] @@ -823,8 +822,8 @@ def calculate_bids( unit.outputs["rl_actions"].append(actions) # store results in unit outputs as series to be written to the database by the unit operator - unit.outputs["actions"][start] = actions - unit.outputs["exploration_noise"][start] = noise + unit.outputs["actions"].at[start] = actions + unit.outputs["exploration_noise"].at[start] = noise return bids @@ -878,9 +877,9 @@ def get_actions(self, next_observation): curr_action += noise else: # if we are not in learning mode we just use the actor neural net to get the action without adding noise - curr_action = self.actor(next_observation).detach() - noise = tuple(0 for _ in range(self.act_dim)) + # noise is an tensor with zeros, because we are not in learning mode + noise = th.zeros(self.act_dim, dtype=self.float_type) curr_action = curr_action.clamp(-1, 1) @@ -924,7 +923,7 @@ def calculate_reward( # Calculate marginal and starting costs marginal_cost = unit.calculate_marginal_cost( - start_time, unit.outputs[product_type].loc[start_time] + start_time, unit.outputs[product_type].at[start_time] ) marginal_cost += unit.get_starting_costs(int(duration_hours)) @@ -937,12 +936,15 @@ def calculate_reward( order_profit = order["accepted_price"] * accepted_volume * duration_hours order_cost = abs(marginal_cost * accepted_volume * duration_hours) - current_soc = unit.outputs["soc"][start_time] - next_soc = unit.outputs["soc"][next_time] + current_soc = unit.outputs["soc"].at[start_time] + next_soc = unit.outputs["soc"].at[next_time] # Calculate and clip the energy cost for the start time unit.outputs["energy_cost"].at[next_time] = np.clip( - (unit.outputs["energy_cost"][start_time] * current_soc - order_profit) + ( + unit.outputs["energy_cost"].at[start_time] * current_soc + - order_profit + ) / next_soc, 0, self.max_bid_price, @@ -993,7 +995,7 @@ def create_observation( end_excl = end - unit.index.freq # get the forecast length depending on the tme unit considered in the modelled unit - forecast_len = pd.Timedelta((self.foresight - 1) * unit.index.freq) + forecast_len = (self.foresight - 1) * unit.index.freq # ============================================================================= # 1.1 Get the Observations, which are the basis of the action decision @@ -1009,7 +1011,7 @@ def create_observation( > unit.forecaster[f"residual_load_{market_id}"].index[-1] ): scaled_res_load_forecast = ( - unit.forecaster[f"residual_load_{market_id}"].loc[start:].values + unit.forecaster[f"residual_load_{market_id}"].loc[start:] / scaling_factor_res_load ) scaled_res_load_forecast = np.concatenate( @@ -1023,16 +1025,15 @@ def create_observation( else: scaled_res_load_forecast = ( - unit.forecaster[f"residual_load_{market_id}"] - .loc[start : end_excl + forecast_len] - .values + unit.forecaster[f"residual_load_{market_id}"].loc[ + start : end_excl + forecast_len + ] / scaling_factor_res_load ) if end_excl + forecast_len > unit.forecaster[f"price_{market_id}"].index[-1]: scaled_price_forecast = ( - unit.forecaster[f"price_{market_id}"].loc[start:].values - / scaling_factor_price + unit.forecaster[f"price_{market_id}"].loc[start:] / scaling_factor_price ) scaled_price_forecast = np.concatenate( [ @@ -1045,9 +1046,9 @@ def create_observation( else: scaled_price_forecast = ( - unit.forecaster[f"price_{market_id}"] - .loc[start : end_excl + forecast_len] - .values + unit.forecaster[f"price_{market_id}"].loc[ + start : end_excl + forecast_len + ] / scaling_factor_price ) diff --git a/assume/strategies/naive_strategies.py b/assume/strategies/naive_strategies.py index 9ab8f53ab..b0023e387 100644 --- a/assume/strategies/naive_strategies.py +++ b/assume/strategies/naive_strategies.py @@ -39,12 +39,14 @@ def calculate_bids( start ) # power output of the unit before the start time of the first product op_time = unit.get_operation_time(start) - min_power, max_power = unit.calculate_min_max_power( + min_power_values, max_power_values = unit.calculate_min_max_power( start, end_all ) # minimum and maximum power output of the unit between the start time of the first product and the end time of the last product bids = [] - for product in product_tuples: + for product, min_power, max_power in zip( + product_tuples, min_power_values, max_power_values + ): # for each product, calculate the marginal cost of the unit at the start time of the product # and the volume of the product. Dispatch the order to the market. start = product[0] @@ -55,11 +57,11 @@ def calculate_bids( start, previous_power ) # calculation of the marginal costs volume = unit.calculate_ramp( - op_time, previous_power, max_power[start], current_power + op_time, previous_power, max_power, current_power ) bids.append( { - "start_time": product[0], + "start_time": start, "end_time": product[1], "only_hours": product[2], "price": marginal_cost, @@ -70,9 +72,7 @@ def calculate_bids( if "node" in market_config.additional_fields: bids[-1]["max_power"] = unit.max_power if volume > 0 else unit.min_power - bids[-1]["min_power"] = ( - min_power[start] if volume > 0 else unit.max_power - ) + bids[-1]["min_power"] = min_power if volume > 0 else unit.max_power previous_power = volume + current_power if previous_power > 0: @@ -115,12 +115,14 @@ def calculate_bids( end_all = product_tuples[-1][1] previous_power = unit.get_output_before(start) op_time = unit.get_operation_time(start) - min_power, max_power = unit.calculate_min_max_power(start, end_all) + _, max_power = unit.calculate_min_max_power(start, end_all) current_power = unit.outputs["energy"].at[start] marginal_cost = unit.calculate_marginal_cost(start, previous_power) + + # calculate the ramp up volume using the initial maximum power volume = unit.calculate_ramp( - op_time, previous_power, max_power[start], current_power + op_time, previous_power, max_power[0], current_power ) profile = {product[0]: volume for product in product_tuples} @@ -159,11 +161,11 @@ def calculate_bids( """ start = product[0] - volume = unit.opt_power_requirement.loc[start] + volume = unit.opt_power_requirement.at[start] marginal_price = unit.calculate_marginal_cost(start, volume) bids.append( { - "start_time": product[0], + "start_time": start, "end_time": product[1], "only_hours": product[2], "price": marginal_price, @@ -192,11 +194,11 @@ def calculate_bids( and the volume of the product. Dispatch the order to the market. """ start = product[0] - volume = unit.flex_power_requirement.loc[start] + volume = unit.flex_power_requirement.at[start] marginal_price = unit.calculate_marginal_cost(start, volume) bids.append( { - "start_time": product[0], + "start_time": start, "end_time": product[1], "only_hours": product[2], "price": marginal_price, @@ -241,17 +243,17 @@ def calculate_bids( start = product_tuples[0][0] end_all = product_tuples[-1][1] previous_power = unit.get_output_before(start) - min_power, max_power = unit.calculate_min_max_power( + _, max_power_values = unit.calculate_min_max_power( start, end_all, market_config.product_type ) bids = [] - for product in product_tuples: + for product, max_power in zip(product_tuples, max_power_values): start = product[0] op_time = unit.get_operation_time(start) current_power = unit.outputs["energy"].at[start] volume = unit.calculate_ramp( - op_time, previous_power, max_power[start], current_power + op_time, previous_power, max_power, current_power ) price = 0 bids.append( @@ -305,23 +307,23 @@ def calculate_bids( start = product_tuples[0][0] end_all = product_tuples[-1][1] previous_power = unit.get_output_before(start) - min_power, max_power = unit.calculate_min_max_power( + min_power_values, _ = unit.calculate_min_max_power( start, end_all, market_config.product_type ) bids = [] - for product in product_tuples: + for product, min_power in zip(product_tuples, min_power_values): start = product[0] op_time = unit.get_operation_time(start) previous_power = unit.get_output_before(start) current_power = unit.outputs["energy"].at[start] volume = unit.calculate_ramp( - op_time, previous_power, min_power[start], current_power + op_time, previous_power, min_power, current_power ) price = 0 bids.append( { - "start_time": product[0], + "start_time": start, "end_time": product[1], "only_hours": product[2], "price": price, diff --git a/assume/units/demand.py b/assume/units/demand.py index 99bc0d9e6..5a50edfad 100644 --- a/assume/units/demand.py +++ b/assume/units/demand.py @@ -2,11 +2,13 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later -import numbers +from datetime import datetime -import pandas as pd +import numpy as np from assume.common.base import SupportsMinMax +from assume.common.fast_pandas import FastSeries +from assume.common.forecasts import Forecaster class Demand(SupportsMinMax): @@ -35,11 +37,11 @@ def __init__( unit_operator: str, technology: str, bidding_strategies: dict, - index: pd.DatetimeIndex, max_power: float, min_power: float, + forecaster: Forecaster, node: str = "node0", - price: float | pd.Series = 3000.0, + price: float = 3000.0, location: tuple[float, float] = (0.0, 0.0), **kwargs, ): @@ -48,7 +50,7 @@ def __init__( unit_operator=unit_operator, technology=technology, bidding_strategies=bidding_strategies, - index=index, + forecaster=forecaster, node=node, location=location, **kwargs, @@ -56,39 +58,39 @@ def __init__( """Create a demand unit.""" self.max_power = max_power self.min_power = min_power + if max_power > 0 and min_power <= 0: self.max_power = min_power self.min_power = -max_power + self.ramp_down = max(abs(min_power), abs(max_power)) self.ramp_up = max(abs(min_power), abs(max_power)) - volume = self.forecaster[self.id] - self.volume = -abs(volume) # demand is negative - if isinstance(price, numbers.Real): - price = pd.Series(price, index=self.index) - self.price = price + + self.volume = -abs(self.forecaster[self.id]) # demand is negative + self.price = FastSeries(index=self.index, value=price) def execute_current_dispatch( self, - start: pd.Timestamp, - end: pd.Timestamp, - ): + start: datetime, + end: datetime, + ) -> np.array: """ Execute the current dispatch of the unit. Returns the volume of the unit within the given time range. Args: - start (pandas.Timestamp): The start time of the dispatch. - end (pandas.Timestamp): The end time of the dispatch. + start (datetime.datetime): The start time of the dispatch. + end (datetime.datetime): The end time of the dispatch. Returns: - pd.Series: The volume of the unit within the gicen time range. + np.array: The volume of the unit for the given time range. """ - return self.volume[start:end] + return self.volume.loc[start:end] def calculate_min_max_power( - self, start: pd.Timestamp, end: pd.Timestamp, product_type="energy" - ) -> tuple[pd.Series, pd.Series]: + self, start: datetime, end: datetime, product_type="energy" + ) -> tuple[np.array, np.array]: """ Calculates the minimum and maximum power output of the unit and returns the bid volume as both the minimum and maximum power output of the unit. @@ -101,9 +103,10 @@ def calculate_min_max_power( """ end_excl = end - self.index.freq bid_volume = (self.volume - self.outputs[product_type]).loc[start:end_excl] + return bid_volume, bid_volume - def calculate_marginal_cost(self, start: pd.Timestamp, power: float) -> float: + def calculate_marginal_cost(self, start: datetime, power: float) -> float: """ Calculate the marginal cost of the unit returns the marginal cost of the unit based on the provided time and power. diff --git a/assume/units/powerplant.py b/assume/units/powerplant.py index 4f907bbff..11905ac78 100644 --- a/assume/units/powerplant.py +++ b/assume/units/powerplant.py @@ -6,9 +6,10 @@ from datetime import datetime, timedelta from functools import lru_cache -import pandas as pd +import numpy as np from assume.common.base import SupportsMinMax +from assume.common.forecasts import Forecaster logger = logging.getLogger(__name__) @@ -26,7 +27,7 @@ class PowerPlant(SupportsMinMax): max_power (float): The maximum power output capacity of the power plant in MW. min_power (float, optional): The minimum power output capacity of the power plant in MW. Defaults to 0.0 MW. efficiency (float, optional): The efficiency of the power plant in converting fuel to electricity. Defaults to 1.0. - additional_cost (Union[float, pd.Series], optional): Additional costs associated with power generation, in EUR/MWh. Defaults to 0. + additional_cost (float, optional): Additional costs associated with power generation, in EUR/MWh. Defaults to 0. partial_load_eff (bool, optional): Does the efficiency vary at part loads? Defaults to False. fuel_type (str, optional): The type of fuel used by the power plant for power generation. Defaults to "others". emission_factor (float, optional): The emission factor associated with the power plant's fuel type (CO2 emissions per unit of energy produced). Defaults to 0.0. @@ -52,11 +53,11 @@ def __init__( unit_operator: str, technology: str, bidding_strategies: dict, - index: pd.DatetimeIndex, + forecaster: Forecaster, max_power: float, min_power: float = 0.0, efficiency: float = 1.0, - additional_cost: float | pd.Series = 0.0, + additional_cost: float = 0.0, partial_load_eff: bool = False, fuel_type: str = "others", emission_factor: float = 0.0, @@ -80,7 +81,7 @@ def __init__( unit_operator=unit_operator, technology=technology, bidding_strategies=bidding_strategies, - index=index, + forecaster=forecaster, node=node, location=location, **kwargs, @@ -124,9 +125,9 @@ def init_marginal_cost(self): def execute_current_dispatch( self, - start: pd.Timestamp, - end: pd.Timestamp, - ): + start: datetime, + end: datetime, + ) -> np.array: """ Executes the current dispatch of the unit based on the provided timestamps. @@ -138,27 +139,26 @@ def execute_current_dispatch( end (pandas.Timestamp): The end time of the dispatch. Returns: - pd.Series: The volume of the unit within the given time range. + np.array: The volume of the unit within the given time range. """ start = max(start, self.index[0]) - max_power = ( - self.forecaster.get_availability(self.id)[start:end] * self.max_power + max_power_values = ( + self.forecaster.get_availability(self.id).loc[start:end] * self.max_power ) - for t in self.outputs["energy"][start:end].index: - current_power = self.outputs["energy"][t] - + for t, max_power in zip(self.index[start:end], max_power_values): + current_power = self.outputs["energy"].at[t] previous_power = self.get_output_before(t) op_time = self.get_operation_time(t) current_power = self.calculate_ramp(op_time, previous_power, current_power) if current_power > 0: - current_power = min(current_power, max_power[t]) + current_power = min(current_power, max_power) current_power = max(current_power, self.min_power) - self.outputs["energy"][t] = current_power + self.outputs["energy"].at[t] = current_power return self.outputs["energy"].loc[start:end] @@ -172,9 +172,10 @@ def calc_simple_marginal_cost( float: The marginal cost of the unit. """ fuel_price = self.forecaster.get_price(self.fuel_type) + co2_price = self.forecaster.get_price("co2") marginal_cost = ( fuel_price / self.efficiency - + self.forecaster.get_price("co2") * self.emission_factor / self.efficiency + + co2_price * self.emission_factor / self.efficiency + self.additional_cost ) @@ -184,18 +185,18 @@ def calc_simple_marginal_cost( def calc_marginal_cost_with_partial_eff( self, power_output: float, - timestep: pd.Timestamp = None, - ) -> float | pd.Series: + timestep: datetime, + ) -> float: """ Calculates the marginal cost of the unit based on power output and timestamp, considering partial efficiency. Returns the marginal cost of the unit. Args: power_output (float): The power output of the unit. - timestep (pd.Timestamp, optional): The timestamp of the unit. Defaults to None. + timestep (datetime.datetime): The timestamp of the unit. Returns: - float | pd.Series: The marginal cost of the unit. + float: The marginal cost of the unit at the given timestamp. """ fuel_price = self.forecaster.get_price(self.fuel_type).at[timestep] @@ -234,23 +235,17 @@ def calc_marginal_cost_with_partial_eff( efficiency = self.efficiency - eta_loss co2_price = self.forecaster.get_price("co2").at[timestep] - additional_cost = ( - self.additional_cost.at[timestep] - if isinstance(self.additional_cost, pd.Series) - else self.additional_cost - ) - marginal_cost = ( fuel_price / efficiency + co2_price * self.emission_factor / efficiency - + additional_cost + + self.additional_cost ) return marginal_cost def calculate_min_max_power( - self, start: pd.Timestamp, end: pd.Timestamp, product_type="energy" - ) -> tuple[pd.Series, pd.Series]: + self, start: datetime, end: datetime, product_type="energy" + ) -> tuple[np.array, np.array]: """ Calculates the minimum and maximum power output of the unit and returns it. @@ -267,28 +262,29 @@ def calculate_min_max_power( """ end_excl = end - self.index.freq - base_load = self.outputs["energy"][start:end_excl] - heat_demand = self.outputs["heat"][start:end_excl] + base_load = self.outputs["energy"].loc[start:end_excl] + heat_demand = self.outputs["heat"].loc[start:end_excl] + capacity_neg = self.outputs["capacity_neg"].loc[start:end_excl] - capacity_neg = self.outputs["capacity_neg"][start:end_excl] # needed minimum + capacity_neg - what is already sold is actual minimum min_power = self.min_power + capacity_neg - base_load # min_power should be at least the heat demand at that time - min_power = min_power.clip(lower=heat_demand) + min_power = min_power.clip(min=heat_demand) - available_power = self.forecaster.get_availability(self.id)[start:end_excl] + available_power = self.forecaster.get_availability(self.id).loc[start:end_excl] # check if available power is larger than max_power and raise an error if so if (available_power > self.max_power).any(): raise ValueError( f"Available power is larger than max_power for unit {self.id} at time {start}." ) + max_power = available_power * self.max_power # provide reserve for capacity_pos - max_power = max_power - self.outputs["capacity_pos"][start:end_excl] + max_power = max_power - self.outputs["capacity_pos"].loc[start:end_excl] # remove what has already been bid max_power = max_power - base_load # make sure that max_power is > 0 for all timesteps - max_power = max_power.clip(lower=0) + max_power = max_power.clip(min=0) return min_power, max_power diff --git a/assume/units/steel_plant.py b/assume/units/steel_plant.py index 11b609a45..9ee3245d8 100644 --- a/assume/units/steel_plant.py +++ b/assume/units/steel_plant.py @@ -3,8 +3,8 @@ # SPDX-License-Identifier: AGPL-3.0-or-later import logging +from datetime import datetime -import pandas as pd import pyomo.environ as pyo from pyomo.opt import ( SolverFactory, @@ -14,6 +14,8 @@ ) from assume.common.base import SupportsMinMax +from assume.common.fast_pandas import FastSeries +from assume.common.forecasts import Forecaster from assume.common.market_objects import MarketConfig, Orderbook from assume.common.utils import get_products_index from assume.units.dsm_load_shift import DSMFlex @@ -36,7 +38,6 @@ class SteelPlant(DSMFlex, SupportsMinMax): bidding_strategies (dict): The bidding strategies of the unit. technology (str): The technology of the unit. node (str): The node of the unit. - index (pd.DatetimeIndex): The index for the data of the unit. location (tuple[float, float]): The location of the unit. components (dict[str, dict]): The components of the unit such as Electrolyser, DRI Plant, DRI Storage, and Electric Arc Furnace. objective (str): The objective of the unit, e.g. minimize variable cost ("min_variable_cost"). @@ -53,9 +54,9 @@ def __init__( id: str, unit_operator: str, bidding_strategies: dict, + forecaster: Forecaster, technology: str = "steel_plant", node: str = "node0", - index: pd.DatetimeIndex = None, location: tuple[float, float] = (0.0, 0.0), components: dict[str, dict] = None, objective: str = None, @@ -70,7 +71,7 @@ def __init__( technology=technology, components=components, bidding_strategies=bidding_strategies, - index=index, + forecaster=forecaster, node=node, location=location, **kwargs, @@ -419,18 +420,22 @@ def determine_optimal_operation_without_flex(self): "Termination Condition: ", results.solver.termination_condition ) - self.opt_power_requirement = pd.Series( - data=instance.total_power_input.get_values() - ).set_axis(self.index) + opt_power_requirement = [ + pyo.value(instance.total_power_input[t]) for t in instance.time_steps + ] + self.opt_power_requirement = FastSeries( + index=self.index, value=opt_power_requirement + ) self.total_cost = sum( instance.variable_cost[t].value for t in instance.time_steps ) # Variable cost series - self.variable_cost_series = pd.Series( - data=instance.variable_cost.get_values() - ).set_axis(self.index) + variable_cost = [ + pyo.value(instance.variable_cost[t]) for t in instance.time_steps + ] + self.variable_cost_series = FastSeries(index=self.index, value=variable_cost) def determine_optimal_operation_with_flex(self): """ @@ -462,14 +467,20 @@ def determine_optimal_operation_with_flex(self): "Termination Condition: ", results.solver.termination_condition ) - temp = instance.total_power_input.get_values() - self.flex_power_requirement = pd.Series(data=temp) - self.flex_power_requirement.index = self.index + flex_power_requirement = [ + pyo.value(instance.total_power_input[t]) for t in instance.time_steps + ] + self.flex_power_requirement = FastSeries( + index=self.index, value=flex_power_requirement + ) # Variable cost series - temp_1 = instance.variable_cost.get_values() - self.variable_cost_series = pd.Series(data=temp_1) - self.variable_cost_series.index = self.index + flex_variable_cost = [ + instance.variable_cost[t].value for t in instance.time_steps + ] + self.flex_variable_cost_series = FastSeries( + index=self.index, value=flex_variable_cost + ) def switch_to_opt(self, instance): """ @@ -542,8 +553,8 @@ def set_dispatch_plan( self.calculate_cashflow(product_type, orderbook) for start in products_index: - current_power = self.outputs[product_type][start] - self.outputs[product_type][start] = current_power + current_power = self.outputs[product_type].at[start] + self.outputs[product_type].at[start] = current_power self.bidding_strategies[marketconfig.market_id].calculate_reward( unit=self, @@ -551,12 +562,12 @@ def set_dispatch_plan( orderbook=orderbook, ) - def calculate_marginal_cost(self, start: pd.Timestamp, power: float) -> float: + def calculate_marginal_cost(self, start: datetime, power: float) -> float: """ Calculate the marginal cost of the unit based on the provided time and power. Args: - start (pandas.Timestamp): The start time of the dispatch. + start (datetime.datetime): The start time of the dispatch. power (float): The power output of the unit. Returns: @@ -565,9 +576,10 @@ def calculate_marginal_cost(self, start: pd.Timestamp, power: float) -> float: # Initialize marginal cost marginal_cost = 0 - if self.opt_power_requirement[start] > 0: + if self.opt_power_requirement.at[start] > 0: marginal_cost = ( - self.variable_cost_series[start] / self.opt_power_requirement[start] + self.variable_cost_series.at[start] + / self.opt_power_requirement.at[start] ) return marginal_cost diff --git a/assume/units/storage.py b/assume/units/storage.py index 36f46d282..68a8e1793 100644 --- a/assume/units/storage.py +++ b/assume/units/storage.py @@ -3,12 +3,14 @@ # SPDX-License-Identifier: AGPL-3.0-or-later import logging -from datetime import timedelta +from datetime import datetime, timedelta from functools import lru_cache -import pandas as pd +import numpy as np from assume.common.base import SupportsMinMaxCharge +from assume.common.fast_pandas import FastSeries +from assume.common.forecasts import Forecaster from assume.common.market_objects import MarketConfig, Orderbook from assume.common.utils import get_products_index @@ -33,8 +35,8 @@ class Storage(SupportsMinMaxCharge): initial_soc (float): The initial state of charge of the storage unit in MWh. efficiency_charge (float): The efficiency of the storage unit while charging. efficiency_discharge (float): The efficiency of the storage unit while discharging. - additional_cost_charge (Union[float, pd.Series], optional): Additional costs associated with power consumption, in EUR/MWh. Defaults to 0. - additional_cost_discharge (Union[float, pd.Series], optional): Additional costs associated with power generation, in EUR/MWh. Defaults to 0. + additional_cost_charge (float, optional): Additional costs associated with power consumption, in EUR/MWh. Defaults to 0. + additional_cost_discharge (float, optional): Additional costs associated with power generation, in EUR/MWh. Defaults to 0. ramp_up_charge (float): The ramp up rate of charging the storage unit in MW/15 minutes (negative value). ramp_down_charge (float): The ramp down rate of charging the storage unit in MW/15 minutes (negative value). ramp_up_discharge (float): The ramp up rate of discharging the storage unit in MW/15 minutes. @@ -57,18 +59,19 @@ def __init__( unit_operator: str, technology: str, bidding_strategies: dict, - max_power_charge: float | pd.Series, - max_power_discharge: float | pd.Series, + forecaster: Forecaster, + max_power_charge: float, + max_power_discharge: float, max_soc: float, - min_power_charge: float | pd.Series = 0.0, - min_power_discharge: float | pd.Series = 0.0, + min_power_charge: float = 0.0, + min_power_discharge: float = 0.0, min_soc: float = 0.0, initial_soc: float = 0.0, soc_tick: float = 0.01, efficiency_charge: float = 1, efficiency_discharge: float = 1, - additional_cost_charge: float | pd.Series = 0.0, - additional_cost_discharge: float | pd.Series = 0.0, + additional_cost_charge: float = 0.0, + additional_cost_discharge: float = 0.0, ramp_up_charge: float = None, ramp_down_charge: float = None, ramp_up_discharge: float = None, @@ -80,19 +83,18 @@ def __init__( min_down_time: float = 0, downtime_hot_start: int = 8, # hours downtime_warm_start: int = 48, # hours - index: pd.DatetimeIndex = None, location: tuple[float, float] = (0, 0), node: str = "node0", **kwargs, ): super().__init__( id=id, + unit_operator=unit_operator, technology=technology, + bidding_strategies=bidding_strategies, + forecaster=forecaster, node=node, location=location, - bidding_strategies=bidding_strategies, - index=index, - unit_operator=unit_operator, **kwargs, ) @@ -105,8 +107,8 @@ def __init__( self.max_power_discharge = abs(max_power_discharge) self.min_power_discharge = abs(min_power_discharge) - self.outputs["soc"] = pd.Series(self.initial_soc, index=self.index, dtype=float) - self.outputs["energy_cost"] = pd.Series(0.0, index=self.index, dtype=float) + self.outputs["soc"] = FastSeries(value=self.initial_soc, index=self.index) + self.outputs["energy_cost"] = FastSeries(value=0.0, index=self.index) self.soc_tick = soc_tick @@ -157,7 +159,7 @@ def __init__( self.warm_start_cost = warm_start_cost * max_power_discharge self.cold_start_cost = cold_start_cost * max_power_discharge - def execute_current_dispatch(self, start: pd.Timestamp, end: pd.Timestamp): + def execute_current_dispatch(self, start: datetime, end: datetime) -> np.array: """ Executes the current dispatch of the unit based on the provided timestamps. @@ -165,50 +167,53 @@ def execute_current_dispatch(self, start: pd.Timestamp, end: pd.Timestamp): Returns the volume of the unit within the given time range. Args: - start (pandas.Timestamp): The start time of the dispatch. - end (pandas.Timestamp): The end time of the dispatch. + start (datetime.datetime): The start time of the dispatch. + end (datetime.datetime): The end time of the dispatch. Returns: - pd.Series: The volume of the unit within the given time range. + np.array: The volume of the unit within the given time range. """ time_delta = self.index.freq / timedelta(hours=1) - for t in self.outputs["energy"][start : end - self.index.freq].index: + for t in self.index[start : end - self.index.freq]: delta_soc = 0 - soc = self.outputs["soc"][t] - if self.outputs["energy"][t] > self.max_power_discharge: - self.outputs["energy"][t] = self.max_power_discharge - elif self.outputs["energy"][t] < self.max_power_charge: - self.outputs["energy"][t] = self.max_power_charge + soc = self.outputs["soc"].at[t] + + if self.outputs["energy"].at[t] > self.max_power_discharge: + self.outputs["energy"].at[t] = self.max_power_discharge + elif self.outputs["energy"].at[t] < self.max_power_charge: + self.outputs["energy"].at[t] = self.max_power_charge elif ( - self.outputs["energy"][t] < self.min_power_discharge - and self.outputs["energy"][t] > self.min_power_charge - and self.outputs["energy"][t] != 0 + self.outputs["energy"].at[t] < self.min_power_discharge + and self.outputs["energy"].at[t] > self.min_power_charge + and self.outputs["energy"].at[t] != 0 ): - self.outputs["energy"][t] = 0 + self.outputs["energy"].at[t] = 0 # discharging - if self.outputs["energy"][t] > 0: + if self.outputs["energy"].at[t] > 0: max_soc_discharge = self.calculate_soc_max_discharge(soc) - if self.outputs["energy"][t] > max_soc_discharge: - self.outputs["energy"][t] = max_soc_discharge + if self.outputs["energy"].at[t] > max_soc_discharge: + self.outputs["energy"].at[t] = max_soc_discharge time_delta = self.index.freq / timedelta(hours=1) delta_soc = ( - -self.outputs["energy"][t] * time_delta / self.efficiency_discharge + -self.outputs["energy"].at[t] + * time_delta + / self.efficiency_discharge ) # charging - elif self.outputs["energy"][t] < 0: + elif self.outputs["energy"].at[t] < 0: max_soc_charge = self.calculate_soc_max_charge(soc) - if self.outputs["energy"][t] < max_soc_charge: - self.outputs["energy"][t] = max_soc_charge + if self.outputs["energy"].at[t] < max_soc_charge: + self.outputs["energy"].at[t] = max_soc_charge time_delta = self.index.freq / timedelta(hours=1) delta_soc = ( - -self.outputs["energy"][t] * time_delta * self.efficiency_charge + -self.outputs["energy"].at[t] * time_delta * self.efficiency_charge ) self.outputs["soc"].at[t + self.index.freq] = soc + delta_soc @@ -243,19 +248,19 @@ def set_dispatch_plan( for start in products_index: delta_soc = 0 - soc = self.outputs["soc"][start] - current_power = self.outputs[product_type][start] + soc = self.outputs["soc"].at[start] + current_power = self.outputs[product_type].at[start] # discharging if current_power > 0: max_soc_discharge = self.calculate_soc_max_discharge(soc) if current_power > max_soc_discharge: - self.outputs[product_type][start] = max_soc_discharge + self.outputs[product_type].at[start] = max_soc_discharge time_delta = self.index.freq / timedelta(hours=1) delta_soc = ( - -self.outputs["energy"][start] + -self.outputs["energy"].at[start] * time_delta / self.efficiency_discharge ) @@ -265,14 +270,16 @@ def set_dispatch_plan( max_soc_charge = self.calculate_soc_max_charge(soc) if current_power < max_soc_charge: - self.outputs[product_type][start] = max_soc_charge + self.outputs[product_type].at[start] = max_soc_charge time_delta = self.index.freq / timedelta(hours=1) delta_soc = ( - -self.outputs["energy"][start] * time_delta * self.efficiency_charge + -self.outputs["energy"].at[start] + * time_delta + * self.efficiency_charge ) - self.outputs["soc"][start + self.index.freq :] = soc + delta_soc + self.outputs["soc"].loc[start + self.index.freq :] = soc + delta_soc self.bidding_strategies[marketconfig.market_id].calculate_reward( unit=self, @@ -283,7 +290,7 @@ def set_dispatch_plan( @lru_cache(maxsize=256) def calculate_marginal_cost( self, - start: pd.Timestamp, + start: datetime, power: float, ) -> float: """ @@ -299,19 +306,10 @@ def calculate_marginal_cost( """ if power > 0: - additional_cost = ( - self.additional_cost_discharge.at[start] - if isinstance(self.additional_cost_discharge, pd.Series) - else self.additional_cost_discharge - ) + additional_cost = self.additional_cost_discharge efficiency = self.efficiency_discharge - else: - additional_cost = ( - self.additional_cost_charge.at[start] - if isinstance(self.additional_cost_charge, pd.Series) - else self.additional_cost_charge - ) + additional_cost = self.additional_cost_charge efficiency = self.efficiency_charge marginal_cost = additional_cost / efficiency @@ -356,102 +354,86 @@ def calculate_soc_max_charge( return power def calculate_min_max_charge( - self, start: pd.Timestamp, end: pd.Timestamp, product_type="energy" - ) -> tuple[pd.Series]: + self, start: datetime, end: datetime, product_type="energy" + ) -> tuple[np.array, np.array]: """ Calculates the min and max charging power for the given time period. This is relative to the already sold output on other markets for the same period. It also adheres to reserved positive and negative capacities. Args: - start (pandas.Timestamp): The start of the current dispatch. - end (pandas.Timestamp): The end of the current dispatch. + start (datetime.datetime): The start of the current dispatch. + end (datetime.datetime): The end of the current dispatch. product_type (str): The product type of the storage unit. Returns: - tuple[pd.Series]: The minimum and maximum charge power levels of the storage unit in MW. + tuple[np.array, np.array]: The minimum and maximum charge power levels of the storage unit in MW. """ end_excl = end - self.index.freq - base_load = self.outputs["energy"][start:end_excl] - capacity_pos = self.outputs["capacity_pos"][start:end_excl] - capacity_neg = self.outputs["capacity_neg"][start:end_excl] + base_load = self.outputs["energy"].loc[start:end_excl] + capacity_pos = self.outputs["capacity_pos"].loc[start:end_excl] + capacity_neg = self.outputs["capacity_neg"].loc[start:end_excl] - min_power_charge = ( - self.min_power_charge[start:end_excl] - if isinstance(self.min_power_charge, pd.Series) - else self.min_power_charge - ) - min_power_charge -= base_load + capacity_pos - min_power_charge = min_power_charge.clip(upper=0) + min_power_charge = self.min_power_charge - (base_load + capacity_pos) + min_power_charge = min_power_charge.clip(max=0) - max_power_charge = ( - self.max_power_charge[start:end_excl] - if isinstance(self.max_power_charge, pd.Series) - else self.max_power_charge + max_power_charge = self.max_power_charge - (base_load + capacity_neg) + max_power_charge = np.where( + max_power_charge <= min_power_charge, max_power_charge, 0 ) - max_power_charge -= base_load + capacity_neg - max_power_charge = max_power_charge.where( - max_power_charge <= min_power_charge, 0 - ) - - min_power_charge = min_power_charge.where( - min_power_charge >= max_power_charge, 0 + min_power_charge = np.where( + min_power_charge >= max_power_charge, min_power_charge, 0 ) # restrict charging according to max_soc - max_soc_charge = self.calculate_soc_max_charge(self.outputs["soc"][start]) - max_power_charge = max_power_charge.clip(lower=max_soc_charge) + max_soc_charge = self.calculate_soc_max_charge(self.outputs["soc"].at[start]) + max_power_charge = max_power_charge.clip(min=max_soc_charge) return min_power_charge, max_power_charge def calculate_min_max_discharge( - self, start: pd.Timestamp, end: pd.Timestamp, product_type="energy" - ) -> tuple[pd.Series]: + self, start: datetime, end: datetime, product_type="energy" + ) -> tuple[np.array, np.array]: """ Calculates the min and max discharging power for the given time period. This is relative to the already sold output on other markets for the same period. It also adheres to reserved positive and negative capacities. Args: - start (pandas.Timestamp): The start of the current dispatch. - end (pandas.Timestamp): The end of the current dispatch. + start (datetime.datetime): The start of the current dispatch. + end (datetime.datetime): The end of the current dispatch. product_type (str): The product type of the storage unit. Returns: - tuple[pd.Series]: The minimum and maximum discharge power levels of the storage unit in MW. + tuple[np.array, np.array]: The minimum and maximum discharge power levels of the storage unit in MW. """ end_excl = end - self.index.freq - base_load = self.outputs["energy"][start:end_excl] - capacity_pos = self.outputs["capacity_pos"][start:end_excl] - capacity_neg = self.outputs["capacity_neg"][start:end_excl] + base_load = self.outputs["energy"].loc[start:end_excl] + capacity_pos = self.outputs["capacity_pos"].loc[start:end_excl] + capacity_neg = self.outputs["capacity_neg"].loc[start:end_excl] - min_power_discharge = ( - self.min_power_discharge[start:end_excl] - if isinstance(self.min_power_discharge, pd.Series) - else self.min_power_discharge - ) - min_power_discharge -= base_load + capacity_neg - min_power_discharge = min_power_discharge.clip(lower=0) + min_power_discharge = self.min_power_discharge - (base_load + capacity_neg) + min_power_discharge = min_power_discharge.clip(min=0) - max_power_discharge = ( - self.max_power_discharge[start:end_excl] - if isinstance(self.max_power_discharge, pd.Series) - else self.max_power_discharge - ) - max_power_discharge -= base_load + capacity_pos - max_power_discharge = max_power_discharge.where( - max_power_discharge >= min_power_discharge, 0 + max_power_discharge = self.max_power_discharge - (base_load + capacity_pos) + + # Adjust max_power_discharge using np.where + max_power_discharge = np.where( + max_power_discharge >= min_power_discharge, max_power_discharge, 0 ) - min_power_discharge = min_power_discharge.where( - min_power_discharge < max_power_discharge, 0 + # Adjust min_power_discharge using np.where + min_power_discharge = np.where( + min_power_discharge < max_power_discharge, min_power_discharge, 0 ) # restrict according to min_soc - max_soc_discharge = self.calculate_soc_max_discharge(self.outputs["soc"][start]) - max_power_discharge = max_power_discharge.clip(upper=max_soc_discharge) + max_soc_discharge = self.calculate_soc_max_discharge( + self.outputs["soc"].at[start] + ) + max_power_discharge = max_power_discharge.clip(max=max_soc_discharge) return min_power_discharge, max_power_discharge diff --git a/assume/world.py b/assume/world.py index 797f5e3f4..74c30e2a4 100644 --- a/assume/world.py +++ b/assume/world.py @@ -9,7 +9,6 @@ from datetime import datetime from pathlib import Path -import pandas as pd from mango import ( RoleAgent, activate, @@ -157,7 +156,6 @@ def setup( start: datetime, end: datetime, simulation_id: str, - index: pd.Series, save_frequency_hours: int = 24, bidding_params: dict = {}, learning_config: LearningConfig = {}, @@ -195,20 +193,15 @@ def setup( self.forecaster = forecaster self.bidding_params = bidding_params - self.index = index # create new container - container_kwargs = {} + container_kwargs = {"mp_method": "fork"} if sys.platform == "linux" else {} if self.addr == "world": container_func = create_ec_container - container_kwargs = { - "addr": self.addr, - } + container_kwargs.update({"addr": self.addr}) elif isinstance(self.addr, tuple): container_func = create_tcp_container - container_kwargs = { - "addr": self.addr, - } + container_kwargs.update({"addr": self.addr}) else: container_func = create_mqtt_container container_kwargs = { @@ -474,7 +467,6 @@ def create_unit( return unit_class( id=id, unit_operator=unit_operator_id, - index=self.index, forecaster=forecaster, **unit_params, ) @@ -625,11 +617,11 @@ async def async_run(self, start_ts: datetime, end_ts: datetime): start_ts (datetime.datetime): The start timestamp for the simulation run. end_ts (datetime.datetime): The end timestamp for the simulation run. """ - logger.info("activating container") + logger.debug("activating container") # agent is implicit added to self.container._agents async with activate(self.container) as c: await tasks_complete_or_sleeping(c) - logger.info("all agents up - starting simulation") + logger.debug("all agents up - starting simulation") pbar = tqdm(total=end_ts - start_ts) # allow registration before first opening diff --git a/assume_cli/cli.py b/assume_cli/cli.py index 639777dfd..5f7a9c7ce 100644 --- a/assume_cli/cli.py +++ b/assume_cli/cli.py @@ -156,6 +156,8 @@ def cli(args=None): study_case=args.case_study, ) + logging.info(f"loaded {args.scenario} - {args.case_study}") + if world.learning_config.get("learning_mode", False): run_learning( world, @@ -163,7 +165,6 @@ def cli(args=None): scenario=args.scenario, study_case=args.case_study, ) - world.run() except KeyboardInterrupt: diff --git a/docs/source/release_notes.rst b/docs/source/release_notes.rst index db4f22280..c3e90bc45 100644 --- a/docs/source/release_notes.rst +++ b/docs/source/release_notes.rst @@ -13,6 +13,14 @@ Upcoming Release The features in this section are not released yet, but will be part of the next release! To use the features already you have to install the main branch, e.g. ``pip install git+https://github.com/assume-framework/assume`` +**Improvements:** +- **Performance Optimization:** Switched to a custom `FastSeries` class, which is based on the pandas Series + but utilizes NumPy arrays for internal data storage and indexing. This change significantly improves the + performance of read and write operations, achieving an average speedup of **2x to 3x** compared to standard + pandas Series. The `FastSeries` class retains a close resemblance to the pandas Series, including core + functionalities like indexing, slicing, and arithmetic operations. This ensures seamless integration, + allowing users to work with the new class without requiring significant code adaptation. + **Bugfixes:** - **Tutorials**: General fixes of the tutorials, to align with updated functionalitites of Assume - **Tutorial 07**: Aligned Amiris loader with changes in format in Amiris compare (https://gitlab.com/fame-framework/fame-io/-/issues/203 and https://gitlab.com/fame-framework/fame-io/-/issues/208) diff --git a/examples/examples.py b/examples/examples.py index d9f5754ef..321b3fd8c 100644 --- a/examples/examples.py +++ b/examples/examples.py @@ -101,11 +101,16 @@ - local_db: without database and grafana - timescale: with database and grafana (note: you need docker installed) """ + + # select to store the simulation results in a local database or in timescale + # when using timescale, you need to have docker installed and can access the grafana dashboard data_format = "local_db" # "local_db" or "timescale" + + # select the example to run from the available examples above example = "small" if data_format == "local_db": - db_uri = f"sqlite:///./examples/local_db/assume_db_{example}.db" + db_uri = "sqlite:///./examples/local_db/assume_db.db" elif data_format == "timescale": db_uri = "postgresql://assume:assume@localhost:5432/assume" diff --git a/examples/inputs/example_02a/config.yaml b/examples/inputs/example_02a/config.yaml index 0979c4805..f6e39adbb 100644 --- a/examples/inputs/example_02a/config.yaml +++ b/examples/inputs/example_02a/config.yaml @@ -3,123 +3,134 @@ # SPDX-License-Identifier: AGPL-3.0-or-later base: + start_date: 2019-03-01 00:00 end_date: 2019-03-31 00:00 + time_step: 1h + learning_mode: true + save_frequency_hours: null + learning_config: - algorithm: matd3 - batch_size: 256 continue_learning: false - device: cpu + trained_policies_save_path: null + max_bid_price: 100 + algorithm: matd3 + learning_rate: 0.001 + training_episodes: 50 episodes_collecting_initial_experience: 5 - gamma: 0.99 + train_freq: 24h gradient_steps: -1 - learning_rate: 0.001 - max_bid_price: 100 - noise_dt: 1 - noise_scale: 1 + batch_size: 256 + gamma: 0.99 + device: cpu noise_sigma: 0.1 - train_freq: 24h - trained_policies_save_path: null - training_episodes: 100 + noise_scale: 1 + noise_dt: 1 validation_episodes_interval: 5 - learning_mode: true + markets_config: EOM: - market_mechanism: pay_as_clear - maximum_bid_price: 3000 - maximum_bid_volume: 100000 - minimum_bid_price: -500 - opening_duration: 1h - opening_frequency: 1h operator: EOM_operator - price_unit: EUR/MWh product_type: energy + start_date: 2019-03-01 00:00 products: - - count: 1 - duration: 1h - first_delivery: 1h + - duration: 1h + count: 1 + first_delivery: 1h + opening_frequency: 1h + opening_duration: 1h volume_unit: MWh - save_frequency_hours: null - start_date: 2019-03-01 00:00 - time_step: 1h + maximum_bid_volume: 100000 + maximum_bid_price: 3000 + minimum_bid_price: -500 + price_unit: EUR/MWh + market_mechanism: pay_as_clear + base_lstm: + start_date: 2019-03-01 00:00 end_date: 2019-03-31 00:00 + time_step: 1h + learning_mode: true + save_frequency_hours: null + learning_config: - actor_architecture: lstm - algorithm: matd3 - batch_size: 256 continue_learning: false - device: cpu - early_stopping_steps: 10 - early_stopping_threshold: 0.05 + trained_policies_save_path: null + max_bid_price: 100 + algorithm: matd3 + learning_rate: 0.001 + training_episodes: 50 episodes_collecting_initial_experience: 5 - gamma: 0.99 + train_freq: 24h gradient_steps: -1 - learning_rate: 0.001 - max_bid_price: 100 - noise_dt: 1 - noise_scale: 1 + batch_size: 256 + gamma: 0.99 + device: cpu noise_sigma: 0.1 - train_freq: 24h - trained_policies_save_path: null - training_episodes: 50 + noise_scale: 1 + noise_dt: 1 validation_episodes_interval: 5 - learning_mode: true + early_stopping_steps: 10 + early_stopping_threshold: 0.05 + actor_architecture: lstm + markets_config: EOM: - market_mechanism: pay_as_clear - maximum_bid_price: 3000 - maximum_bid_volume: 100000 - minimum_bid_price: -500 - opening_duration: 1h - opening_frequency: 1h operator: EOM_operator - price_unit: EUR/MWh product_type: energy + start_date: 2019-03-01 00:00 products: - - count: 1 - duration: 1h - first_delivery: 1h + - duration: 1h + count: 1 + first_delivery: 1h + opening_frequency: 1h + opening_duration: 1h volume_unit: MWh - save_frequency_hours: null - start_date: 2019-03-01 00:00 - time_step: 1h + maximum_bid_volume: 100000 + maximum_bid_price: 3000 + minimum_bid_price: -500 + price_unit: EUR/MWh + market_mechanism: pay_as_clear + tiny: + start_date: 2019-01-01 00:00 end_date: 2019-01-05 00:00 + time_step: 1h + learning_mode: true + save_frequency_hours: null + learning_config: - actor_architecture: mlp - algorithm: matd3 - batch_size: 64 continue_learning: false - device: cpu + trained_policies_save_path: null + max_bid_price: 100 + algorithm: matd3 + learning_rate: 0.001 + training_episodes: 10 episodes_collecting_initial_experience: 3 - gamma: 0.99 + train_freq: 24h gradient_steps: -1 - learning_rate: 0.001 - max_bid_price: 100 - noise_dt: 1 - noise_scale: 1 + batch_size: 64 + gamma: 0.99 + device: cpu noise_sigma: 0.1 - train_freq: 24h - trained_policies_save_path: null - training_episodes: 10 + noise_scale: 1 + noise_dt: 1 validation_episodes_interval: 5 - learning_mode: true + actor_architecture: mlp + markets_config: EOM: - market_mechanism: pay_as_clear - maximum_bid_price: 3000 - maximum_bid_volume: 100000 - minimum_bid_price: -500 - opening_duration: 1h - opening_frequency: 1h operator: EOM_operator - price_unit: EUR/MWh product_type: energy + start_date: 2019-01-01 00:00 products: - - count: 1 - duration: 1h - first_delivery: 1h + - duration: 1h + count: 1 + first_delivery: 1h + opening_frequency: 1h + opening_duration: 1h volume_unit: MWh - save_frequency_hours: null - start_date: 2019-01-01 00:00 - time_step: 1h + maximum_bid_volume: 100000 + maximum_bid_price: 3000 + minimum_bid_price: -500 + price_unit: EUR/MWh + market_mechanism: pay_as_clear diff --git a/examples/inputs/example_03/config.yaml b/examples/inputs/example_03/config.yaml index b314ad356..4fa68ae59 100644 --- a/examples/inputs/example_03/config.yaml +++ b/examples/inputs/example_03/config.yaml @@ -7,6 +7,7 @@ base_case_2019: end_date: 2019-02-01 00:00 time_step: 1h industrial_dsm_units: null + save_frequency_hours: null markets_config: EOM: diff --git a/examples/notebooks/01_minimal_manual_example.ipynb b/examples/notebooks/01_minimal_manual_example.ipynb index c6fa06e58..66f54c05e 100644 --- a/examples/notebooks/01_minimal_manual_example.ipynb +++ b/examples/notebooks/01_minimal_manual_example.ipynb @@ -109,7 +109,6 @@ " end=end,\n", " save_frequency_hours=48,\n", " simulation_id=sim_id,\n", - " index=index,\n", ")" ] }, diff --git a/examples/notebooks/03_custom_unit_example.ipynb b/examples/notebooks/03_custom_unit_example.ipynb index 0820c9a62..697fe8f33 100644 --- a/examples/notebooks/03_custom_unit_example.ipynb +++ b/examples/notebooks/03_custom_unit_example.ipynb @@ -50,13 +50,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": { "vscode": { "languageId": "shellscript" } }, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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1MUnP/nJjrc3rXM5IRsX/+pXv0r7afCGUPfpppuu+OhHP1q+rO3npvnijLf6UYZSgMpruMrHP/7xtEfECy+8kNpBv7TCvZNUeWjH119/PbWne/WT2WefvVMy6Z7f/e53KZyXvjnVVFMVM800U8f7ml6g3e3BoO9/8IMfTGkPVB9uWoZW11UZtrqufNw9OBuf+rV9QfRn7TjFFFOUL02fy3mY+8w5xHFGsLp70gX//FO+v/y5fE23n80nr776aqqDPTe0i7E4/fTTd5tUo+tfeuml4je/+U261jxmnskcGiXQh4uMp2uvvTbNAeprHthggw2KT3ziE31I7f9umXnmmf/vnw6fBqq9OmQzyWlzoHGnf9pzRpnVf3I/exXUXMDwm5879nZRvrzXyySViQNBIAgEgSAQBILAqCUQRpFR27RRsSAQBIJAEAgCQaBKgFLmrLPOKu64446kTKS43mOPPYpZZ521emnb/21QLSyK9Chb11hjjWK11VZrq2j72c9+Vlx++eUpNJH9HORNoUkpSQksDaFVCGVWE5H/KaecUjz44IO9lx900EFJSapcF110UXHPPff0GkR4A0h71113LZZddtnee8ofbr311uKmm25KSstcTvdQGlHyUyAtv/zy6VW+r+ln7M4+++ziqaeeSoopRgpCIf6xj30sMdhkk02SwpZB5pvf/GZSmqrPRhttVKy00kpNsypuv/32Qn0oRSn75YXBBz7wgVSXGWecMcXsb5Im1tLThrhg8uUvf7lYaKGFUnnuvvvudI6y13n5aF/MKKzXXHPNtKly48IP4YXjxo0rnn/++dSfMWKkKMubb75ZHHrooamv47DwwgsXW221Vbrk8ccfLy655JKCEhhj/ZpQPhtrdSI9HG0kTVn917/+tTfEEGU1BflnPvOZYsMNN+yzovz6668vvBgQ9J3cHsYbo+D6669fzD///HXFa3mMQvXkk09O/Ul6K664YrHWWmulvtvypn+eoCQ+7LDDkqIYw89+9rPFNtts0+m23vM///nPi6uvvjq1k/bBmUGBYUN/Vi9GBW2zzDLL9Bri5GVfiwsuuCAxYIh0vePY2ABcW1XnHOkussgixRe/+MXeMrjnjDPOKMxlxD3mQYrlJvLMM88U9rB57rnnkoFSu0vDODH+GQ+Vn/GgidECg/POOy/N58rLi2LzzTdPHPTF888/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3fGKq4NAvwgwFPjSSqlLgSxkkQ2uKdsp159++uli9913T8aTL37xi70KXBth20fERu4Uv5T7lF95RbCNs7MiWgEp1y+++OKk0HfNtNNOW2RFvjR++tOfJuXxYYcdlvaXKFeKAYIi3YuymlGA8cQeFFNMMUV6d71VCFYlqw9ltY3fX3jhheLjH/94Mcsss6Q9LCj8GTyEbVprrbUmMYy47+ijj06KcWkrpxejxn333VfccccdaaPzffbZZxIDRrnM1c8MRN///vdTftj5gqC80mXEwV3ZlAv/LNoBZ3u34DbddNMl1owLjCKuLRtoMssJEyak63CaddZZ070MMXvssUcygv3gBz+YxAAlPcaxU045pTdtPLUdwwAj1EILLTQJs1zWdu/SYAgjc845Z3q1u77VOcz0K21T9QDJ97hGv3ZN3Ybwuc+/8847xZ577pkMVdp4pplmShu8O7/hhhum/vjWW2+lPXMY0epEn/jd736XjDPLLLPMRJc899xzyfhjDGk3eTAQaXP96MYbbyy++c1vJmNTeaxMlEj8EwRGOIHsSRUrRUd4Q0bxg0AfCZgDPJc7eV32Mfm4LQgEgWFKIP9WaeVJPUyLHcUKAkFggAjQLZgH4jfAAAGNZMYMgTCKjJmm7n9FX3nllaT89mOrqZiYKZop1KtiU2eK0m5EehTTM8wwwyS3Uaj+6le/Sg+DSU6WDnhg8ILgjTA5hEL2xBNPTOW0el7YIop7fHla3HDDDcnzwor9vKG6a3hOCIF07rnnJk+HAw88MBke1AEXxon8mYcEowADxeqrr57y4IGg7SiNpXHXXXelFf6U8lbTl8UXajxt0s2jgtKasp8RgMK9LBThxx13XArlteuuuxaU1ZT7NivnBcAA8cMf/jAx502S+490bADOC0U5eUesscYa6bN25E1wySWXJB6MQBTqTYVxQr3lhyEFuXylq948dG7t2VxcnXiHZIX/N77xjeTpwMODl8NGG22UvBF4HBDXqVsWxgttOe+88yZONmankFc3Cnz1v+eee1JbHHnkkfm29K4M5513XjKgrLTSSsXGG2/cy/imm25KRi1eOt0o8ClDlPHhhx9O6eoXSy655ET5DvU/uHsxVJkHMNZH9PnXXnsttTeDyrge75vXX3+94JXSyijCYwp/1xrDWXgm8aR69dVXi+WWWy71JZ468nj77beTYYqRkOePNl922WV72zynEe9BYDQQMIaMe2MkJAgEgbFFwPdb3yV9Hywv+BhbFKK2QWBsEvBbxXeAvKBrbFKIWgeBsUtAtAqLZ/3WjYURY7cfRM27JxBGke6Zjdk7rPh/5JFHurI++4G23nrr1RpFrIS3mj0ryZuAlZ7wOXVGEXshUCa7pl2avCq23377yWYUUTbGIAYHxo4sFLhejEUU94wXFLseaiz+zln5TuGVV+RPOeWU+fbedyvpKX8ZOtRz/fXX7z3nAwMBD4QvfelLxYsvvpjCPgl5VBZlZBQZ3xO6SqisLGXjiXIQ1zI2HHLIISksV76WgcBDmTHjZz/7WXHqqacmr5O8eoE3hk3A/XhXRuGQcrupK08aXgS8Kaz0Fw5LuZvIwgsvXCy++OKTXEopz0jCiCE/Cvhnn302hdRycTZ45HIwSChvHWceJww6vGK++93vThJaau211y7mmGOO4utf/3px++23Fw899FAK3yQfinqeC5T5jEH7779/b94YCwvlnWErl8V9nSR7DjEOGAfkU5/6VKfbBv08I5F23nbbbQtcsnz0ox9NH51nxDImcGWQyka+fK0xox/pG/pWWU466aSkCGIQOfjgg3s9rFwjD94yxgwjFiPgYostljyhymnE5yAwGgiY//M8MBrqE3UIAkGgOQHfy4RqJWEUac4trgwCo4GAKATGv4VkIUEgCIw9An4/C01NNxFGkbHX/lHjvhP4l77fGneONQIU8pTGFMVNX65vNSlTNjdNp3xdVqpX+ef0OpXR+ewZUE1jKP73pdXq/boV/LxqxINVPqGohP7JQsmdDRH5PZ8rv1O+26/B3hlVg0i+Tmgse1YwEDE21In8rOrvJK6zV4N9SqoibBXDCgU/zwyeJsQ9jFgMIwwHPCXqlP9bbrllMoAx0PC4aCoU4FXhRSFEGQV85izElVVVraSdcpGXhz639NJLT2IQyekxwOT9VHgAZWGI4QXCQ2KzzTbrNYjk894p/hdddNFJPHPK19R91nd8Kcp9ZHJ5RJXLpr3tR4JVnWgv5/RLYbDKm73n66+99trEwo89fSoLw5I9XYwVoefynir5fH7nhcRApL/7whgSBEYjAeO/bi4djXWNOgWBIDApgZgDJmUSR4LAWCDg2R/jfyy0dNQxCNQTMP69Ro306Gx6YpEXPeE3mleJvqhnr9OeWPXN74krxzyB8BQZ812gOQDKfK65rYwSdSlRhnrVSU7Pl7imShxp5Zjp1TSz0ts17dJTh3bK7mq6A/2/ctqAXBmqBiP/ZyW2Fb+t2LUrE+MBDwuhiXggMALUCW8FbfnHP/4xGSvK3hAU6kJQNfEyUMZWym75CvtltT6XToYeXhwY8AhQX2G9eJTUCSU5VkJptTNe1N0rDx43PJJ4slCa53bHRHq57nX3dzomPJYvHuphb5K6tlI/hhfCeyMLIw+PIEaTVqs5MRP+S9ipbqU8pnKdu01joK9niMqeIXVpM6DxBMIV0/KG764XOotxFBPeIll4VBkrzvE0qY4p1+Fh3PM2wQN7e5qEBIEgEASCQBAIAkEgCASBIBAEgkAQCALDhACjRk9Y9uKYY4rissuKoicqRE+oBzHj/bCfuJD5WE9Um+LLXy56woAUPT/8i57wEUWPsmvia+O/IFBDIHpJDZQ4VE9AHH5Ky3YGh+qdlOt1oa5cJ4QNb4ZuJCvr6+6xEp3iNa+Qr7vGMcpr100uUT6Gj1Ycs1KXUr9TXerqQNlPWW8FvZBVrfLJqwl4bzAOlI0iFMfCbDUR17ZTMEuHkUZ+b775ZkrSPYwyyiZfCu06cV5buZ5Xi3LyrugklN72OWEgYoBwrzQIpsoiPJO+gHO3Ii2KeGk9+uijad+UVpwZnuSnHFnE+2SYyWzy8fK7+ximuu0DyoZpLo/N7CenKL+6lPtXXXl4gAh/xaOI5wdDVjYYPfPMM8m4pB+tuuqqE93O2MbghbE9alrxwkM59AVMXJcZTZRg/BMERjAB/brVGBjB1YqiB4Eg0JBAzAENQcVlQWCUEchjP74DjLKGjeoEgYYEjP2s72h4y/C9TLSSnn1bk/TsU1tstVXRo9gqeuKjT2wY6alzzw/6oidWedETG77oiQ3/j3vs5cqw0rMfb0gQ6EQgjCKdCMX5XgJWcnsNlNgIzmugxH4RdRu6D1T6A5nOYCpjKdulL6SVvUIogdsJb4y6FfwUyE2lLlxVvpeRJ9e37E2RP1Nmt5NcjqYPeiGybHDOu4SxZZNNNkn7i+i7/hdeiSL9mJ6VB0Ja9eXHw/9n7zzg7aqq/L+Gkt4bL410SEIPAwlJCCENCFKUpoLIKEVFQGUUdfQ/DFhwrGMZKY6iqCCiMDAEkJYACSEJkJDeC+m99+D/fHfcL/fdd+t797537t2//fncd+87+5xdvnvvc/bZa6+1WISnTpQd81f4uUmnkePrligU8BMW6ubZ+PMSv2GXT/l8Wj0izRvPFXNUCBvqM1AWL+xLVw7aBa0ihGCYuFoWadh4ocgzzzzjOKAhkmx2DqEWfQmW999/f1oBm8+Xc/Ep41n54/oWgXIggLDX31/KoT6qgwiIQO4EeK6x6YZ5nYIIiEBYBJgHs8Eo0ztZWERUWxEIiwAm4rkHlMU7Lv5DH3nksMYHzRitZ1hkVj1y6HpYMOKb1gtEiFu+3B+1aCJU9bwjMfolAtUI5L7qWe1SHRABEYgjAV6IWfhlVz2aPbksqtfm4cm13rFnKh5oonhtDO88m2v8b4QY6QJlZ1c/57Nonk6jJPF6THRhNgsGmA8bNWqUW5D3QgLOZYG+NguHTDo8VwRK+P7w/yeWJd1vXw/YYEYuXcA3SD5t4/1pnHHGGY4X1+Lk/ZZbbkmXRcbjiXmnqx9tyyfx3IyJZohEcIV5NYQg+BWh//KSh1N60k8l3KEtOAc/IWeffXal+bkM2RSkrJnSV5wI1BcBnCsqiIAIhEmAuU3v3r31jAuz+VXrwAmgSY+fxkLMxwNHqeqLQEkSYDMhG44T1zxKsiIUGiFHtJYTLWIc8Q8SrfHYDTccFowMGnS4aq+/fliLJFEgEq0NRCZDLNoZe/gc/RWBLAQyb9HOcrGiRUAE6pZALhPdfv36OYEAi8QIK7gm26c2teAlfObMmWmTwOk7i/sIIU444QR3Htfgr4SFdsxoYeorVWCxHYfkLPbjrN0v+qc61x9DiIL5LF4M+HBN8uSA8qCJQDlShVw4s/BAnTCfRcjGODFNzGKh1bQkUvGkLKkCJrYylTHVNf4YeSFA4Js03nnnHR+V1zeCNQQOpEN5UglG4O0FV3klnuJkJnL4kKG9Jk+e7EyUTZo0yeVNGS699NJqV+GTBi0c31c4L9unWiI6IAJlQoCxk3y/K5OqqRoiIAI5EGD88wxUEAERCI+Axn94ba4ai4AnwLO/rN4BMIf1wAPsZvVVPKwx8qlPmU2fbjZ1qhm/o7WOytCggdmDD5pdd13lIf0QgWwEJBTJRkjxIhATAjzoWKQmoAWRLuD7hQcigoHnnnsu3WnuOIv6LCbXJrBojjNwtB5SBbQ2EHwgEPHmkBBUYN6LaxdEzrCm82BLETD9hCklfG+cdNJJKc6ofsgv3KOBkkoLg2NvvfWWc3yeTiiCJgdsMpnEuuCCC9w5+LvAB0amAGPS8wETUb169XJtRP1StcH777/vfKLkIgjy6VI3r3lz4403GloU/P/9738/rfDFX0ufos/g08MHBDfsPIMTwh/P1sfzPTey8wmDdCwTz83lN36G6Cu0EY7Rn3/+edeO7IBHyJUccLxOv8JcwMORrVFMm2UKqfpEpvMVJwKlRAAhKIJoBREQgfAI8BxftGhRpf+28AioxiIQLgHMJfNOhc9DBREQgfAI4I+TtZNM60QlRwXBCEKOaM2oMuBjJNpE6RyvR3OeysA6GX5IJBCpRKIfuRGQUCQ3TjpLBOqdAEKRjh07usV1fGKwYMwiNYvtiQvu+JP48Ic/7Pxm4Fvj5ZdfdgviLPDzkOQbAQbaGa9HKofE1zawEH3vvfe6iTjps8jPYvwTTzzhTFnx/8c+9rEqC+fdu3d32gwsxOMgG3NJpOOvXR6pQd59991usRsBwnnnnZdTMfE7gRkmFgdxtI7GDAvhfNB2eOONN5w/Ec5LF+DM+X5x0TNOFApcffXVzv8FLx/f+ta3bN68ec4RPNdRB+pCftTvL3/5i2HWy4cTTzzRaUQgcMAPBoIRnMhzHfxWrFjh/KKsXLnSCY78ddm+KR9tTHkrKirszjvvdII0BBefjxyW8U2ZPGfKiqYKkyhMVv1LNPFIFFAhVKGdsE9OW8LUX0v7UufHH3/cLcAUSiiCBg4+P/bs2ePKhKYITD7ykY+krD6Ctcsuu8w5padtfxo5VGNscD08+fCbfoBGE/WgDgoiUI4EuLdrQaQcW1Z1EoHsBJgDoLnJfEJBBEQgLALM6Rn/muOG1e6qrQh4AghGeQdIXBfycSX9jTYIghHMYvkQvd9Hix7+P4t2cR4WiHziE0eO6ZcI5EggQeSW4xU6TQREoF4IsPh7+umnO18c7777rv3oRz9yC+scZyKMuSQW+lmcvuOOO2zVqlU2bdo0+8pXvuKcU3Mti/C8LLO4PTVSOcR802233WZoPdQ0sOg8duxY57QcQQZpsZjOAvQrr7ziFt2HDRtml1xySRWhCL44brrpJid4QJtk7dq1NmbMGCf4Wb9+vT355JNusZ5F8k9ED7hUzuBTlRkBCs64EWj85Cc/sdmzZ9tpp53mzEnwG60WTIyhdYNQKFUYPny4TZgwwQlQSANBEwHhwODBgx1HNBO++93v2te+9jWXx6eiBzb+S9ByQLuDFxMEISzUs0j/pz/9qTIrNHkuv/xyJ6TATNTtt9/uFvYpO9fBDcEX3Phd04CwgEXSRyJHZVOmTHEczz33XEMbg76CYAMBDFoZCEzQxkCLJTGQBv2InSef/exnncCNtqB/IVDDhw18Sb8QAQdxCEWo92OPPeYEf7DO1Ec/9KEPub7817/+Ndog8isniKMNvRARoQ/aPAhY8LeCGS6vdVWIMisNEYgLAYTnfBREQATCJKB7QJjtrlqLgB/7mgOoL4hAmAT8PaAsa4/GCAEBSaqAma1rr00Vo2MikJWAhCJZEekEEcidAJJ5dqV7bYHkK9m94zUCkuP8/1zLOal2+rBojiCBXf180AQhT3b/s8DvtR8we3TffffZr3/9a7egjcbEs88+63bNs2DPeWhTIKjAmXViIH92GrBgnktAKIJmCoIGNCIQIqB9wKJz586dDXNeLPp7x+qJabKYfnekDfK73/3OCRYeiB5o8MN8Fdci6LnyyitTaon4cnJ+YkAg89HIsRYMEUhgQozFcp8mC+Is9GNqCW6UPzlcfPHFbpEf4ROaHL5Nu3bt6hbqvZAEk16YpvrNb37jfJ/QJn6HBvnhB4VrYMACfWLApwpcEALg8+O3v/2t6zcIGWD5pS99yWlIeM2GRC2VxHSy/b4uUiFFsPT00087M1cIBsaNG+f6F32B/OgLMBkyZIhddNFFVZKkjp/+9KedRgiaIdSXSRcmzXBsjjDoqaeecs7Q0ehILif9mP5Ee+UaEOCh6YJZLtIbPXq088GS6XpYUg+csiPAeTDaUUK7UVaEgfgrGThwoI0cOTIn3zSZ8lKcCIiACIiACIiACIiACIiACIiACIhAcQiwjlC5kTHa1Jk2RGsnCiJQUwISitSUnK4TgRQEWChG8wJBBTvSEwOL9SzwI4To379/2t28aCJ8/etfd+dUPgT+kRBp3HLLLS4NtD1Y+CcvFqOTF90py1133eVMHLFI7M1Iseuenf5oBKDVgPYDC89+Z9E555zjBAick6uzLgQo10bSeeqGY3QEAyy4IxBgIRoBQbqAY218RSCAwKSXF4qwwI3QBMFCqkA54UH5kwMCFUxHIYxAewU+nIcmBGWkrmi3ICwgj+SAAAdzYGhx+DJhlgqH3snlwRTWt7/9bWd2Cq0L6s65lA3hFHmimZEq4A/jG9/4hjOfheYF18EdTQnKhm1g+gLtRN41DYMGDXICDDRl4OHV62kjhDL0BfokZU4VEBJRB7R/Nm/e7NoWoQUs4UF6fNPWaC75QF+jz3Ocds410B8+85nPGObD6N+MiVyCF6DRBzFb5oUitCd+R2BKv1IQgXIlwHhJFkyWa11VLxEQgeoEdA+ozkRHRCAEAjz7/SeE+qqOIiACVQkw/pkDlENAGOLXLcZGm28bRht8o8WB9FWLfKk6p+x5rDekT0wxoRFwNhaiAfT30CpejvXFETILvdwMf/GLX6S1wZ9YdxxgY9qHRWIWDBMXNBPP0+8wCHAr8MKRbDXG3NWtt97qzC499NBDTssg2zWKLy4BtF74IEzLtR2LW6LSTx2ntWi5IDDq3r176VdINShrAgjLmQOgiaYgAiIQFgE2VmAWlY0AyRtlwiKh2opAeATYKMdmIjYBecsB4VFQjUUgXAKszWAFg3eAUl7TYzMvVjQwQ741egf/VLTJtu8PfmDRopNFCxwWSX+rNrI/Flm3sP/5H4t2Yx4+h+MKIpADgSNbenM4WaeIgAiUNwEtpJd2++JPho/asbTbUaUXgZoSQEtO+1xqSk/XiUBpE+D5j3Zqrlq+pV1blV4ERCCRAJt3MO9byouhifXRbxEQgfwItGvXzlluYC5QigF/qpg+RyiyM/KJuj/a5NkvsnDRKdrsbdFGbhcQiET1tO98x6KdoBaZ1LDIZMXhuMgihn3ykxbZNTeLLIo44YkEI6XYFeq8zBKK1DlyZSgKvrxaAABAAElEQVQCIiACxSEgYUhxuCpVESgVApjEUxABEQiXAGYrFURABMIjgDBU4z+8dleNRcATKFWBKFYu8I2LKXX80n4Qab0eiEyv94mEIx9+8klr4YUeVDTyhWo/+pHZJz5xuNqR4CSyXW+RA9PD/0f+SJ1gJPLVKsHIYST6m53AUdlP0RkiIAIiIAKlQAAzT/h30U7xUmgtlVEECk8A0zmYfFMQAREIjwDms+ZHCwL41FIQAREIiwDms+bNm+f8/IVVc9VWBEQAAvhBZQ6AkKFUAn5AMfuPhgimv1jDOBRt7ugT+VVFINIqUSCC79Of//yIQIRKfupTZj/7mUUOT49UORKm2A03mE2adNjc1pEY/RKBlASkKZISiw6KgAjkQoAHlxbgcyFVN+e4iUS0KMK3tEbqhrlyEYE4Edgd7ZRiYVRBBEQgPAI8+9llKY2x8NpeNRYBnv07IpMzbdu2FQwREIEACeCcnDlAKazN4Nf42WefNXwh+jULvvEW0iCaywx7662qAhG0YB94oKpAxLcxghHCzTdb9BJ0+HckVHEaJYMHH/5ff0UgAwEJRTLAUZQIiEB6AhUVFfbXv/41/QmKEQEREAEREAEREAEREAEREAEREAEREAERCJoAFi0mTJhgb775puPgBSL8w+9OHTvamLFjrdtnPmN2ySVmkydHUpJIIPLQQ6kFIp5msmDkvPPMHnzQx+pbBDISkFAkIx5FioAIiIAIiIAIiIAIiIAIiIAIiIAIiIAIiIAIiIAI1ITAjBkzbOrUqe5SpxkSaYXgGL5Vq1Z21lln2cCBAys1R+z//s/sggvM7rgjs0DEF8QLRhCgRKa3rHVrH6NvEchIQEKRjHgUKQIiIAIiIAIiIAKlQQCV+VJQmy8NmiqlCJQeAY3/0mszlVgECkHAP/91DygETaUhAqVHwN8D4lxyhB5z5851ZrM++OADZ+6vd+/eNnTo0MiHeuREPTFgCnDKFLOj8nCDjWAEfyL5XJOYp34HSUBCkSCbXZUWAREoRwLsuDgqmgQkqqKWYz1VJxEQgdQEmjRpYrxkKIiACIRHgGd/s2bNrFGjRuFVXjUWgcAJHHPMMda8eXP5FAq8H6j64RLg2c8cIC7rAPv3748sX0Wmr5LCeZFpK3yfdIxMZaEdcvzxxyedkfBvTYQbNbkmIUv9DI+AhCLhtblqLAIiUKYEeCFCBTUuk6EyxaxqiUBsCXTt2jW2ZVPBREAEikuATRE9evRwmyOKm5NSFwERiBuBxo0bW8+ePY13AQUREIHwCLRv397atGnj1gLqs/Zr1qyxOXPmuA0aQ4YMqVYU5iljI78h3K8URCAOBPTUjEMrqAwiIAIiUAACCEMkECkASCUhAiVKINWOrBKtiootAiKQJwGe/w0bNszzKp0uAiJQDgQQimr8l0NLqg4iUDMCCETrUyiK9sfbb7/tzGOtWrXKmcbq27ev+06ukQQiyUT0f30SyMNAW30WU3mLgAiIgAhkI3Dw4EHbu3evfApkA6V4EShTAsuWLbMlS5aUae1ULREQgUwEDh06ZAsXLrS1a9dmOk1xIiACZUhg9+7dtmDBAtu2bVsZ1k5VEgERyEZg3bp1bg7AXKCuA8KQxx9/3F577TVDUwThzPbt2238+PF1XRTlJwJ5E5CmSN7IdIEIiIAIxJMAvgSYCOFoTRoj8WwjlUoEiklgx44d7h5QzDyUtgiIQDwJ8OxnQRQzmgoiIAJhEThw4IBt3brVWrVqFVbFVVsREAFHAMEocwDWA+pqHrBixQqbMGGCrVy50vbt2+fWH9BaYz2CtQj8HFEejimIQFwJSCgS15ZRuURABERABERABEQgDwK8gEggmgcwnSoCZUZA47/MGlTVEYEcCfjnv+4BOQLTaSJQZgT8PaAuqrVr1y57+eWXbfbs2YZDdb8hk28EIt27d7cLL7zQKioqJBCpiwZRHrUiIKFIrfDpYhEQAREQAREQAREQAREQAREQAREQAREQAREQAREoTwJofWAq65VXXnEmu30tEcgQ16xZMycMOfnkk7VJy8PRd+wJSCgS+yZSAUVABERABERABERABERABERABERABERABERABESgKgG0NIoZ3n//fXvmmWcqfYYk5oW5rsGDB9v5559fr87eE8uk3yKQKwEJRXIlpfNEQAREoAQI1KXqbAngUBFFICgCODaU3d6gmlyVFYEqBI499tg6syVeJWP9IwIiUK8EmP8z/jUHqNdmUOYiUG8EeAfgHsC9oBjhueees7Vr1zqhhzeXRZ69evWyUaNGWbt27YqRrdIUgaITkFCk6IiVgQiIQH0RWLdunW3cuNEaNmxovXv3rq9i1Fm+TISKORmqs4ooIxEQgRoR6NOnT42u00UiIAKlT4DFif79+2tRtPSbUjUQgbwJYLYGkzV15WA57wLqAhEQgaIS6Ny5s3Xs2LFomhqXXXaZPfDAA85nSIMGDVxegwYNcvOOolZMiYtAkQlIKFJkwEo+LAI7d+60adOmOZuKiTVn1w6L1SzOt2jRwtq0aSNpeiKgIv1+5JFH7Fe/+pWdcMIJ9tRTT5X9QkGxdoYUqXmUrAiIQIEJaDGkwECVnAiUGAEEIwoiIALhEeAdQOM/vHZXjUXAE2C9qRCaYgcPHnTpJKd13HHHGUKQhQsX2mmnnWZnnXWWW9vy+etbBEqVgGbOpdpyKncsCaxatcpuu+0227dvnx04cMB9/CS1cePG1rx5c0OKj5rhgAED7Mwzz3QL9sWozIYNG+zpp592+fEAa9WqVTGyiXWaPMxZJCzGQuGyZcvsjTfeMNqVnRNxeBFhEoOTM2mLxLpbqnAiUDQCqLVzD+jUqVPR8lDCIiAC8STA2F+9erWbl7Rt2zaehVSpREAEikJg7969xrtf69atnbPjomSiREVABGJLYPPmzbZ7926nwVGTtQ/WEebMmeNMZJ166qlWUVFRra7Dhg1zGml6z6iGRgdKmICEIiXceCp6/AiwCI/go1GjRvbP//zPdtJJJzkVQ15UmaxiymnRokVOawG7jKeffrpdccUV9uEPf7jglcEZ1l133eUcXvXo0SNIocjIkSOdUKhly5YFt685a9Ysu+eee6x9+/Y2duzYWAhF6Gf79+93ZZHWSMGHlBIUgdgTWL9+vXvm6GUl9k2lAopAwQkwB1izZo3TRpZQpOB4laAIxJoAG/IQimKVAFNaCiIgAmER2Lp1q1trQqMjX6HIkiVL7N1333XrVNu3b3fXs8aRnA5rXHrHCKtfhVBbCUVCaOVSr+Pf/27RinbJ1WLo0KF27bXXOm0RnFEhfUd6v2XLFps3b5794he/sBkzZrgJLFolV199dUHr6AU0aDIkqz8WNKMYJ4ZqJ59iBDRDmjZtak2aNClG8jVOU8KQGqPThSJQ8gS41/O8URABEQiTAPcAzQPCbHvVOmwCjHuN/7D7gGofNgHGf7IQIxuRTZs22aRJkwyhCJt3uZ41jrffftu6d+/urJtkS0PxIlDqBI4q9Qqo/CVIINrJZv/xH2Zf+YpFUoLMFfACkUhyHak8WCRFyHx+TGL9xJTiYMoIZ1Qsnrdr185whHvJJZfYL3/5S+vSpYsh1f/DH/5g8+fPL2jpWbBn1yAPtxB3DBV7YdALQ5iAIHhSEAEREAEREAEREAEREAEREAEREAEREIG4Ejh06JCNHz/eHn30Uachguktbwqcjbysc6B1piACIRCQpkgIrRynOkY3WfvZz8y+9S2LVCcOl4zfkdAgZUBD5L33zG680eydd8wuuMDslVfM+vdPeXopHcSk1b333muf/OQnnbmDP//5z/aNb3yjWhXefPNNmzx5ss2dO9cwjUJALRLzXJjdQtCSGDDTdd111zmNFMwncB15JD7oRo8ebV/+8pcrL8MXyssvv+w0VzC7RRoIUvr27etMQ5FXTQJp/ed//mfUdO/YAw884GxQ/u///q89//zztm7dOmdmDL8qH/vYx7KqYi5evNgef/xxmz17tu3YscOZKTvllFPsyiuvTLmLAcHUww8/bE888YTb6fDTn/60UmMGgQn+Vu677z679NJL7Qtf+ILjxcRg+vTphtooqqFjxoypZtps5cqV9q//+q+uLRA4oflz0UUXVe7M3LNnj91www320Y9+tHIywc4Ln7bfhdGmTRvr1q2bnX322XbuuefGTuOkJu2ta0RABERABERABERABERABERABERABOJHAL8hEyZMMLREEID4zbwISjCPhfnxM844Q2sT8Ws6lahIBCQUKRJYJZuGwJNPmn3pS0civ/99i1aqzb79bYtUGo4c97+iBfBKgQjHooV0O+ccsxUrzCI/EaUecLaOwAEbjgsWLHBaI4kO0W+//XabMmWKe2DxoPLaDyui+k+bNs0t+CNYYWHdB87BpvTOnTudlgqmubwwhXN4+LGQ78PMmTOdUAJtB/JAu4TAA5IyIcC48MIL7f/9v//nL8n52+eFAARbt5/73Ods6tSprgyUEy0LNGQQyJA+DuGTA+VHYPSTn/zElc1zoHxc+2TUp/7lX/7FbrnlluRLHQPq3qJFi2pxmDKDEz448O/y0EMPOUGNrz/CD4Q5qI9SNrR9COSPM+Nt27Y5LRxvw9tnsGvXLidU8W2FUAsBFPVP5Lt06VLX7o899pg9+OCDNnjw4ErBik9L3yIgAiKQDwHuO/7ek891OlcERKA8COgeUB7tqFqIQL4E/NjXHCBfcjpfBMqDAGPfr2OkqhEbM1944QVnKsufx3qKD/2jTccXRBuQW7du7Q/pWwSCICChSBDNHKNKRn427CMfMfvrX48U6nvfOywQiRb3nYDEx0QaDtFKt0Wr6P7I4e/IuXU5CESoDA+icyIhDwIOBBULFy60s846q7K+LKSjrTF8+HDntB2BCYv5aDM89dRThvbE3Xffbb/61a8qNS2Q8KNRgbDj85//vKGJgTCiZ8+eblGexBPNabFQj9Dgsssuc3l37tzZnYfA4C9/+YsTYiA06Nixo910002VZcvnByqY348EYJQXp+TkhXkvBD5oUKBRgtbGD3/4wypaHzywX3311Uix6Ftu5wK7FtAq6d69u7uGeiO4+fWvf+12M3ziE5/Ip1hO0IFACqEMpsxuvfVWO/HEE51wCkEMEwfyP/nkk53mB4mjQfLHP/4xUlh6xX70ox+5iQOm0OBOeZmQMJngfzRuvv71r7vv448/3mnwnHDCCS6O3RnvRVpQCHUSJyR5VSDpZAQ3XniTFKV/RUAEAiCA9pyCCIhAmATQCK6pZm+YxFRrESgfAi2jzYKJm+TKp2aqiQiIQC4EsELCJzmwHoHfED5sNmVTqg+sQbBuwfpMr169/GF9i0BQBCQUCaq5Y1DZaGHdmc/CdFZkvqgyfOc7Fq0mmyHwiF7qIhUAs89+1mzixMpTopVjsx//2OyOO44cK/FfPIgQerCYjtmlDRs2VKnRd7/7XcPMUnLAeTh+STC39be//c0t3F9zzTXORBZpItjwKpHYg2TBv2vXrsnJuP9POukke+mll6qpSLJbAPXJRx55xJnAQjCCSajmzZunTCfTQR6+qGp+9atftY9//OOVpyKAoC7UA7NYzz77rN18881OaMBJmLH6XiQ0wy8LZbkn6h/ef0fv3r0jNzPnR4pENzptjt///vdud0OHDh0q08/2g3IhqBkxYkSkrPTtKsIiyoWAA3NfaIzAG0EOZUEwQj7EY0ILM1ip7G5OjPov7cpCxW9/+9sq7Cj/wIEDnaCJCQrtpiACIiACtSHAPYmg+0ltKOpaEShdAroHlG7bqeQiUFsCGv+1JajrRaB0CaQb/6wvvfbaa65irH1wHusXrKkMGTLEbdAt3Vqr5CJQewJHxIS1T0spiEBuBKIFZfv5z80+9KGq50cCAPu3f7NohdsiFQeLjB2ysnP4HEwXRbvyy0kg4iuPlgYPJ0xNsYCeGFIJRNDs4FziMLnE9xtvvOEk/4nXImjxC2P8ThdY5PdOw/05nE8+fNDMQEMFAQXCgZoE0kH4QlrJ4dRTT7WLL77YPZgx1UU+PmBqCy0SBDpXXXVVpUDEx/ONaSvqibmwcePGJUZl/U094f+pT32qikCEC9G4uPzyyx0DzGWhOZMYPFPazv9OjOc3ZSIeYUomYRJtUIgAZ8yBkaeCCIhAeAR48Uk0lxgeAdVYBMIlwFyE+crWrVvDhaCai0CgBLAuwLtK8rtkoDhUbREIjgCmvZkDJK9L4It2aGSthU2YBNYlsL6BJREsliiIQOgEpCkSeg+or/qjtfCLXxzWDom0AypD5JQ7Uks4/C8CERZ3oxt35JG8LAUiVNQviPMAQ9iRHDCXtXz5cqf5gfYHKpAsfnMdpqPQQsDHCMdqGlhER/jAg3Tz5s3OkbkvC+ny8GSBf/Xq1TXKgoV6/JJQR3YmJAe0UtBuWbRokXuZ99oemNeinjzMMQOWKmCWql+/fk4TZdasWalOSXuMOmJupn379inPoRzsosCxO75C8g1ogyCwYZKCqa/hw4db27Zti2ar0wtF6BNeIJZvmXW+CIhA6RLgHs59gHumggiIQFgEmGMxlyvmPCMsoqqtCJQOAd4PGf+8AyRvdiudWqikIiACNSXAxijWitq1a1fFRBYbPVnvWLZsmVvTQTuE9RMFERCBwwQkFFFPqB8CCDu4Gf/3f1vkyMEiu0mHBSCJpeGcSEMhsq1kdtttR7RGEs8pg99eM4LF/+RJLKad/hr5Xxk/frxzKs4uIL/YjSCDBXuuwXRTTbUD2FH0f//3f+6Dr5JEJ+ykSX5oo+CHBAFNTQIv6ti4TLRhmZgOQgm0UTgPwYsXJjC5R4hCXCphik8DE2SUHYFOPoH6YYM3lekrnw6+QRCeeCGRP57LN9oxF110UWQp7mlnggxTXExKqB+TkT59+jjTW7mkles5vn/ker7OEwERKB8CXi2+fGqkmoiACORDgLmS5gH5ENO5IlAeBBj3zAE0/sujPVULEciHAMIQNoeyljJgwIBql7JZ4sMf/rCzXCH/o9Xw6EDgBCQUCbwD1Fv10QIhIBiJfC1EjiXMVq48fCzxb2TWyO68M/FIWf1mUX5lVG8msSzMs/jvAw+3/4y0ZjCNxfErrrjCaVMgnOCllwcaDz8EJl4d0l+b6zfXsWCP3w52F/MQRWuD/EifiTUP18cee8xpO3BOTQL19L5AUl1PXl67AcGPD/ymrggmMgUvTKKs+YZsLxC1fbm4M+q/OFd/6623bO7cuc55Pbu52MkNa7RHMCuWSTCTb510vgiIgAiIgAiIgAiIgAiIgAiIgAiIQHkSYHMrG0Nnzpxpq1atcusmw4YNq2YFg3UWBCMKIiAC1QlIKFKdiY7UFYFooTxadbfIu7hZwkJ4leyxixwtIEer4lUOl8s/LLjjN4OFeXxOdO/evbJqOB2fMWOGM7WEw3W0Dlq3bl1lBxDOzydNmuSEIgge8g2YhXr44YddmizMX3/99U7lMnkHARoOtbFRTT0zaZkg/EBAQx0QgHhBBL8RxGSzj4tpL66BY9wCQizY4ige0zbr1q2zhQsXOmHWtGnT3CSGcl977bUZtWHiVi+VRwREIH4EavIciF8tVCIREIGaEtA9oKbkdJ0IlDYBP/b9d2nXRqUXARHIRgBBCP5e2WDLhksEH5gsf/nll50v1kxWNrKlrXgRCIlA/FYQQ6Ifcl29QOS998w+/vHDgpFUPB555LD5rBqabUqVZJyObdy40V5//XWnldGxY0fnUNyXjwcdggQcY5177rnOhJUXFvhz8CWCYCP5uI/PNjHGVBZ+PLp16+bMPHXq1MmVxV/PNw9ZfJrURuDAtZgCS6fJgQ1M/G5wHmWgPnwwMYVQBIFMJm2YefPmOd8j2NCsq5DIPPF3uvzxT4JT+dGjR9uNN95o//Vf/+X8rFC/P/zhDxnrly7NVMdzKUuq63RMBESg9AnwQsRHQQREIEwCmGLVQkiYba9ah02Adyie/7V5XwuboGovAqVBAFPjf/zjH43NsayvsEbC2GdNgbUfTIMriIAI5E5AQpHcWenMQhKIFryjLfJmmMeKJNxVQmRGygXOiW7y9sMfmv3Hf1gk+q5yWjn8c/fdd0dKMvusRYsWdvHFF1epEtoRCBHSmZ1CGDJnzhy3I6DKhf/4h8VxHpCkkU44gsCDeM5L1g7xab7wwgsuj9ostpP2888/n1Z4M3v2bLfLATNTiSbEBg4c6B70aFigVZEq4DQMoQjO4PHXUVcBZj7k628EHgjBzjrrLNf27PBIJzDyeeTyzWII/UUvRLnQ0jkiUH4E8FfEfVRBBEQgPALMSzDL2blz5/AqrxqLQOAEeA/CqgB+IBVEQATKjwDrNs8884zbTInVCTbP+k0QrCfhv/Xmm2+2ESNGVB4vPwqqkQgUnoCEIoVnqhRzIRAtgkfb5c3efrvq2T/5iUWets2uuaaq4/XIt4b9+79bJAKven6M/8skREAr4rbIeTx+JljIHjJkiNMgSKwOmgWYj0KTJJX5qCeeeMLefPPNtA89Ft4RMKBlsWvXrsSkK38zccYfB4vy76G1kxQQOPwwEkqlE5gknZ72X17U8afxCJo/SeHtqA/g6J2H+dixY52QwJ+C0IAHPOV7/PHHDTNZyeHfo34Ba8yPcX1dBbRSEDaxK2P9+vUps8XGJ22dLqCBQ9vgaD5Tf0l3ffJx0ihEOsnp6n8REIHSIMDzhI+CCIhAmAQY/4mbNsKkoFqLQHgE2BDF+5o2RoXX9qpx+RNgzehnP/uZvfvuu5UmyXnnZ2MmayBXX321XXfddc76h/yUln9/UA0LS+DIVufCpqvURCA9gWhXv91yi9mUKVXP+fGPze644/AxhCNoifz1r0fOue8+i2Z6Zvfee/j7SEzsfrFYzkL50qVLnaYDC+csfr///vtRtafYhAkTnPYFE9dzzjnH7rnnnmp1QMr/2muv2ebNm51Piptuusk96HDAjo8PHLB37do1csmyIaWWAQ/IE0880TnfQqDAg9NrYeDngkV9vvF18dJLL9n9999va9assfPOO8+VZfLkyW4nAkITfJngC6OmgQf26aefbj/96U9twYIFdumllzphDP5UcOJOvieffLITaiQ+yNn19NWvftVuv/12Gx85lL/11lvtox/9qOMAy1/96leOMcKjG264oU4diPXr188JrRDU/PznP7dPf/rTjicaH3CGGQKtW6K+PmjQICf46tWrl7sGs2kvvviivfrqq848GWUvxEIm/YwPaUk4UtPequtEoHQJcJ/m+VNRUVG6lVDJRUAEakSA+QfzKTRGtVu8Rgh1kQiULAE2l/F+wTsI708KIiACpU+ADZRY28BqRnLgfZ8NpMOHD3frKphFR3uEdwCvQZJ8jf4XARGoTkBCkepMdKSYBJYsMfvMZ8wmTqyaCwKRL3zhyDEWdKIFdCcYidQEK8N3vnPYOfu3vlV5KG4/WJDixZQF+wceeMAVjwVqPjyg2MXDB/MGn/jEJ+zKK69MWQV8iVxyySX25JNP2qpVq5xwgLRJA8HBVVdd5YQi5JNKkwTzTAgfUK/EBBbqllyPgOLyyy+3733ve25nAYIGhDfYp3w4crpOen63EQ/VH/zgB05oQ524viaB8t15550ufZx/oRnC4r3n0bNnT/v6179u3RMczZMP5Rg2bJjdfffd9pNIUIa5sLvuusvxJY4Xfyb+CBWuQbsoRaDM6cqdS53SnUMbfioy//Y///M/Tsj1t7/9zeWD8OuLX/xiJN+7w5WPMqLRgyCMOlMW+gJtSP1pY9IpxM5O0vd2RSUUSdEZdEgEypwAwnjuAxKKlHlDq3oikIIA8xWEImzKkFAkBSAdEoEyJoBpHd4XWSiVUKSMG1pVC4YAz3QsajCuWTPwgTHOOs8FF1zgfLH641gHYfMs1kYSz/fx+hYBEUhNQEKR1Fx0tFgEMJsVLRBXhuimHq3OVxWI+EhsIv/iFxatgJs9+6w/avbooxatjFu0on/kWEx+sVCONsD+BP8nLE6z4M0ElZdU4nG4jb+MbIGFdXYAjBs3zjBlxQOuS5cuNmbMGKfRMWnSJOeMnIXw5Icf/yM4wXE5jrh4oDJhRihCGj706dPHLeyjsYG5J85hlxH5cj2aIqTBYn5tXrK5HsEGdXnllVecJg2CATRIEAwdd9xxvkhVvqnHFVdc4crzl7/8xfkPQTsDnmiXIODBIXuq4HmjUZMsJMAJGZo05JvMzqdFe2KjH60ahBvJAbudCHTQtEGoxC4tBEBMRugDOFQ/88wznT+UJZFAkIkKvmBgilmwUaNGOS2S5HT1vwiIgAjUhAD3ueR7XU3S0TUiIAKlSUD3gNJsN5VaBGpLwI99zQFqS1LXi0A8CLABlPUY1nCwDMK6CZse2DjLWlJy8PeA5OP6XwREIDOByJM1rhui1UqFkicwY8YMZ34IqfIvImHCRz7ykax14gaLqi03URZ/C7FbPWOmdLU//tHs85+3aPXYDM2PaFd9VID0l0Vqg/a5z1m0mm7RCrPZU09ZtKqf/nzFFJUAt4tcJ9xooHzzm980THFhwmvAgAFFLVti4vmUM/G6Uv6NIAYBGQIcJlIKtSewaNGiSqbdkzSZap+6UhCBwhKYNWuW0xQ57bTTCpuwUhMBEYg9ATa98C7AogkbNhREQATCIbB9+3abP3++MVfFV6GCCIhA6RDA0kQ6DS+sUfA+2r9/fycQSbdexwZaNmAiMEGbREEERCA3AtIUyY2TzioUAYQf1157WCAS7aq3aKd9RoEIQpRu3Q6b0op2+zsNEQlECtUaNUonV4FIqsTrUlBRm3KmKruOiYAIiEDcCXCP5aMgAiIQJgGN/zDbXbUWAf/81z1AfUEESocAmxmxJjFz5kwbHvkGwTpFcsA3KZ8WLVokR1X5398DqhzUPyIgAlkJSCiSFZFOKAqByKxQTgEhCgs8kcmpyBN4TpfoJBEIlQCCIP8JlYHqLQIhE0C1Hp8iCiIgAuER4PnfqFEj7RANr+lVYxFwpoAZ/+l2kQuRCIhAvAjgRP29994ztLzRFMHiw8c//vFqhcwmDPEXoB3Ce4A2hnoi+haB3AhIKJIbJ51VnwQQjCiIgAhkJcCLEGazNBnKikoniEBZEsBflHaJlmXTqlIikJUAz//ukekcLYpmRaUTRKDsCGA6F7N5LIoqiIAIxJfAli1bbOrUqc5PKmbseXYj0Fi4cKEzgVlTE7iYzcMvbDpfqfElopKJQP0SkFCkfvkrdxEoawL4t8Ep+tatW52D97KubAwqhzBEE6EYNISKIAL1RKBJkyb1lLOyFQERqG8CzAGaNWtW38VQ/iIgAvVAgPm/xn89gFeWIpAjATS533rrLZs+fbrz6cv/jFs2M6El0qFDh7R+RXLJAoGohKK5kNI5IlCVgIQiVXnoPxEQgQISOP744yOrZ/fbnj17rFOnTtJgKCDbVEnhZBVBFLtNpC2SipCOiUB5E1i7dq17uerYsWN5V1S1EwERqEaA5//q1asN4WibNm2qxeuACIhA+RLYu3evW2hlp7iEI+XbzqpZaRJAC2T8+PG2fv16w48I7+loiPDujnbnmDFjbMCAAbUSimzevNl2795tvANok2Rp9hOVun4ISChSP9yVqwgEQYDF+YqKiiDqGodKsiDCRIvJlYQicWgRlUEE6pbApk2bnE8RCUXqlrtyE4E4EGAOwIJL69atJRSJQ4OoDCJQhwTYac7GCHaKSyhSh+CVlQhkILBt2zYbN26cLV68uNLnH+/oaIfwzo6prFGjRlnLli1rLcjYvn27IRg57rjjap1WhiopSgTKjoCEImXXpKqQCIiACIiACIhAiAR4yZJPkRBbXnUWgcMEdA9QTxCBcAno+R9u26vm8SKAaSw0QyZOnFhtXk4cm0YvvPBC69GjR0E3MuoeEK9+oNKUBgEJRUqjnVRKERABERABERABERABERABERABERABERABERCBmBLYsGGDTZkypYpABA2Rpk2b2sCBA23QoEHOskNMi69iiUBQBCQUCaq5U1cWibJM7aRmo6MiIAIiIAIiUCoEtEOsVFpK5RQBERABERABERABEShHAmiCnHHGGTZ58mRXPYQhPXv2tJEjRxp+f4oReAfQe0AxyCrNcicgoUi5t3AO9fN2DXM4VaeIgAjEnIAEnIVtIGy0K4hAqRDAj5P6bKm0lsopAoUnwD0Av2IKIiACYRFg/t+gQQPnvDmsmqu2IlB/BBBC7N271xo3blytECNGjLBFixZZixYtbPDgwda7d+9q5xTyAM9+7gFaCygkVaUVAgHNmkNo5Qx15OZ51FFHOUdP3NDlmC0DLEWJQMwJMJ6PPvpovRAVqJ1wgIfdVwILTQoiEHcCnTt31i6xuDeSyicCRSLA879bt256XhWJr5IVgTgTYFG2e/fuKRdn41xulU0ESpXApk2bbP78+bZr1y5DAMIzODEgoLjyyiud4/O6EFS0a9fOCWCSy5FYJv0WARGoTkBCkepMgjrSpEkTt6OMhT9sHyIgadSokRZVg+oFqmy5EGD8KhSGwL59+9w90afWsmVL/1PfIhBbAs2bN49t2VQwERCB4hJg0UXPquIyVuoiEFcCbIzS+I9r66hc5URgz549NmfOHJs5c6YtXLjQmHt36tTJTjrppGrVxIxWXQUEo6k0Vuoqf+UjAqVKQEKRUm25ApW7YcOGTjuEBcDdu3fbmjVrjGNeg6RA2SgZERCBOiDg7YjWxW6UOqhOvWQBw4MHD9r+/fudOjSmiLADKy26emkOZZongfXr1ztNkeOOOy7PK3W6CIhAqRPgebVu3Tq3KFIsm+WlzkjlF4FyJcC7PDvXGftselQQAREoPAGEITNmzLDFixcbYw5tENbQpk6dal26dKlXweTWrVsNgQ3vANooWfi2V4rlS0BCkfJt25xr1rZtW3cuEylMaPFhUVULqzkj1IkiEAsCaHyxKIJQU+O3Zk2CUMQLl/jm5bJ9+/Y1S0xXiUAdE0Djk/uAhCJ1DF7ZiUAMCPD8Z3NT69ati+bINQbVVBFEQARSEGCBdvXq1c58noQiKQDpkAjUggBja9KkSbZs2TLbvn27Ezrwvs2cm3duTFfxf30GhCKbN292ZZFQpD5bQnmXGoH6HbmlRqtMy8sNnEU/dkLv3LnTSbvZJe0XBsu02qqWCJQdARZE8IMhoUjNm5aJLf5DUD/mnoiWiGyz1pynrqxbAolCvbrNWbmJgAjEgYDuAXFoBZVBBOqHgN7d64e7ci1fAmheTJgwwZnL2rFjh1sf472QsYZlAbRDRo4caV27dnXWVuqTBGViLUBBBEQgPwISiuTHq2zPRprM4h87S/RCVbbNrIqVMQEmaCtWrDDM53SPHC1iBk+h5gQQjmiXTc356UoREAEREAEREAEREAEREAERKEUC06dPt9dee83QwEDYwLshH7RD8ME7evRoO+2009xmOo7Xd/Dlq+9yKH8RKDUCEoqUWosVuby6mRYZsJIXgSIS8BMyFvOl3VBE0EpaBERABERABERABERABERABESgrAhghnLcuHFus6HfIMc7tt84PHDgQKcdgmBEQQREoPQJSChS+m2oGoiACIiAIyChpjqCCIRNQPeAsNtftRcB3QPUB0QgXAJ+ATdcAqq5CNSOAIKP+fPn29KlS50GSGJq3SNLDBdddFGs/fYxB1AQARHIj4CEIvnx0tkiIAIiEFsCOHlr0aKFNWjQILZlVMFEQASKR6Bbt27FS1wpi4AIxJoAGqJ9+vSpd2evsYakwolAmRLABDbjH594CiIgAjUjgFBh0KBBNmPGDNu0aZMTjLRp08aGDh3qTGXVLNW6uaqiosLatm2rOUDd4FYuZURAQpEyakxVRQREIGwCqPFKlTfsPqDah02gWbNmYQNQ7UUgYAIs5jRv3jxgAqq6CIRL4JhjjnEbo8IloJqLQH4E9u3b5wQIySan8cs5ZswYe+655+zUU0+1IUOGlISwUesA+bW/zhYBT0BCEU9C3yIgAiJQ4gR2795tTPDQFkme4JV41VR8ERCBHAhs27bN2Txu1apVDmfrFBEQgXIigNkPHMKiLdq0adNyqprqIgIikIXAgQMHbMeOHW7ss6irIAIikJrAwYMHbdmyZTZ37lw75ZRTrHtkFisxsMEArSu0Ljp06JAYFevfu3btsv3791vLli1NpvRi3VQqXMwIHBWz8qg4IiACIiACNSSwYcMGW7BggfFipCACIhAeAV7ylixZEl7FVWMREAE7dOiQLVy40HASqyACIhAWATZG8Q6AYFRBBEQgNYFVq1bZSy+9ZE8++aS99tprNmXKFNuzZ0+1k9G8KiWBCBVYu3atuwcwF1AQARHInYA0RXJnpTNFQAREINYE2BUiDZFYN5EKJwJFJaDxX1S8SlwEYk+Ae4B2iMa+mVRAESg4AXa3a/wXHKsSLBMC27dvt2nTpjntEDYOMFYwOTtz5kynFXLaaaeV/LOTOiHMURABEciPgEZNfrx0tgiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIQEwJYFbyrbfecsIPhCGYzjr22GOdqdm9e/facccd54QjMS2+iiUCIlAHBCQUqQPIykIEREAEREAEREAEREAEREAEREAEREAEREAERKC4BDAn98Ybb9i6desMAYi3qIBgBKfkQ4cOtdNPP91at25d8loixSWp1EWgvAlIKFLe7avaiYAIBESA3TAffPBBQDVWVUVABBIJMP51D0gkot8iEBYBxj9zAQUREIGwCPh3AI3/sNpdta1OYMuWLfbiiy/a0qVLnTCEMxCIMDb49O3b10aMGOG0RMrJ3CTPf/kTqd4fdEQEshGQUCQbIcWLgAiIQIkQqKiosHbt2lnDhg1LpMQqpgiIQCEJ9OnTp5DJKS0REIESIoA98ZNOOkm+xUqozVRUESgUAfwjnHLKKdagQYNCJal0RKCkCOzbt88mT57szGUlO09HGNKyZUu76KKLnFCkpCqWY2G7dOlirAXIr0iOwHSaCPyDgIQi6goiIAIiUCYEeBHSy1CZNKaqIQI1INC4ceMaXKVLREAEyoEAjpabNGlSDlVRHURABPIkgFBU4z9PaDq9rAhs27bN5s2bZzt37qzcHMBzEW0QTGUNGzasrM1kaR2grLqzKlOHBCQUqUPYykoEREAEikkAe6n79+93DuPKSR24mMyUtgiUEwFeBAnsGFUQAREIiwA7YXfs2OGcyEpAGlbbq7YigJ+E3bt3O18J2iCl/hAigQ4dOtiZZ55pa9eudWayGAdoUF9wwQXWokWLskeCdsyBAwesefPmhjBIQQREIDcCR+V2ms4SAREQARGIOwEcyc2dO9cJRuJeVpVPBESg8AQWL15sOJZUEAERCI8AtsTZJbtq1arwKq8ai0DgBHbt2mVz5syxrVu3Bk5C1S93AjzrMJWVKpxwwgnWr18/w5TUNddcY1dddVUQAhFYrFmzxs0BEJAqiIAI5E5AmiK5s9KZIiACIhBrAmiHoD6vIAIiECYBjf8w2121FgFPgHuANEU9DX2LQDgE2BnO+NcO8XDaPMSaYiKLDYCbNm2yCy+8sNp7LxohY8aMsaZNmzqtyZAYaR0gpNZWXQtJQEKRQtJUWiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgArUmgIloNKHfffddQysabYiePXs6rZDkxFu1apV8SP+LgAiIQFoCEoqkRaMIERABERABERABERABERABERABERABERABERCBuiaAMGTGjBm2cOFCw2/GMccc4zSixo0bZ926dbMmTZrUdZGUnwiIQBkRkFCkjBpTVREBEQibwAcffOB2zoRNQbUXgXAJsHOO+4CCCIhAmAR0Dwiz3VVrEfj73//u3gE0B1BfKBcCGzZssEmTJjnNEHzlYB4KgQjPOfp7165dZS4yobHxtQIbBREQgfwISCiSHy+dLQIiIAKxJdCxY0dr27atNWjQILZlVMFEQASKRwAHkwoiIAJhEsCfwMknn1zNxnqYNFRrEQiLQLNmzeyUU06xhg0bhlVx1bbsCBw4cMBee+01mzVrliEMQdDH8w1BCGa0MJs1bNgw50y9UaNGZVf/mlYI5/IVFRVOcFTTNHSdCIRIQEKREFtddRYBEShLAghDJBApy6ZVpUQgJwIyIZATJp0kAmVJAAfLOJdVEAERCI8Ai8YIRhREoJQJ4DPk9ddft+3btzutB55raIggKMGJ+sUXX+z8iKivV29lBKISilbnoiMikI2AhCLZCCleBERABEqEAHZW9+3b5yaNTCAVREAEwiLASySBF0cFERCBsAiwi3bbtm1uc4QEpGG1vWorApjN2blzp/OvoA1S6g+lRmDlypX2/PPP29q1aw0zUAQEIjzXeKc966yzbPjw4dayZctSq1qdlXf37t22f/9+xwh2CiIgArkR0KpZbpx0lgiIgAjEnsDGjRtt0aJFbkIU+8KqgCIgAgUnsHz5clu6dGnB01WCIiAC8SfAQtLixYvdolL8S6sSioAIFJIAC6I4okYwqiACpUaA99d169ZVCkQoPwv7nTp1shtuuMEuu+wyCUSyNCr84Ci/IllAKVoEkghIUyQJiP4VAREQAREQAREQgVIkoJ1hpdhqKrMIiIAIiIAIiIAIhEtgyJAhNm/ePCfUxxQcpiDPPfdcpyESLpX8as47gN4D8mOms0UAAhKKqB+IgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAgUhQBmnhF6HHNM1WXIY4891gYPHmzjx4+3Pn362IgRI+QfoygtoERFQASSCVS9GyXH6n8REAEREAEREAEREIGSIIDtZQUREAEREAEREAEREAERiAsBTDrhL2T69OnOJNaAAQOqFe2UU06xzp07W9u2bavF6UB2ArwD6D0gOyedIQLJBCQUSSai/0VABESgRAlIbbZEG07FFoECEcAZpV6ICgRTyYhACRLQPKAEG01FFoECEWAOoCACcSOAMGTOnDk2depU2759u3Xs2NH69u1rTZo0qVJUnl8SiFRBktc/8NM9IC9kOlkEHAEJRdQRREAERKBMCLRu3doaNWpkqCAriIAIhEcAh5QSioTX7qqxCECAxZDjjz9eJkfUHUQgQALM/7t162bNmjULsPaqchwJbNu2zWbOnOk+q1atcmazGjRoYJs2bXJmssaOHRvHYpdsmRAoMf4xT6YgAiKQOwEJRXJnpTNFQAREINYEcErHR0EERCBMAghGFURABMIkgFCkXbt2YVZetRaBwAmw2KzxH3gniEn1P/jgA2cma8aMGfb+++8bprPYsMemnQMHDhhz1Q4dOsSktOVTjObNmxsfBREQgfwISCiSHy+dLQIiIAKxJbB7927bu3evtWzZUrtEYttKKpgIFI8Au/J46WzVqlXxMlHKIiACsSTA2N+yZYuxOKrd4rFsIhVKBIpGgMXmHTt2uM1RDRs2LFo+SlgEMhFYvny5vfHGG04YsmfPHsOkE5oLCEYQ3A8dOtTwJyIzWZko1ixu165dhiN73gFkRqtmDHVVmAQkFAmz3VVrERCBMiTAYsj69eutf//+EoqUYfuqSiKQjQDmCdihJ6FINlKKF4HyI3Do0CFbsWKFG/8SipRf+6pGIpCJAJuiWJDu2rWrTOhlAqW4ohBgQf7FF1+0+fPnuw16COlZmOebvnnyySfb+eef77SZENwrFJ4AZsk2b97stEUkFCk8X6VYvgQkFCnftlXNREAEAiPAYiiLIkxAFURABMIj4O8B4dVcNRYBEYAAcwDuAwoiIAJhEWDuz258vQOE1e71XVueNxMnTrRJkyY5LQX+RzuED9pL7du3twsuuMB69uzptBjru7zlnD/Pfz4KIiAC+RGQUCQ/XjpbBERABERABERABERABERABERABERABERABIIlgEbC9OnTzZvKQhiCYA4TbsOGDbPBgwdLc6mOegfsFURABPIncFT+l+gKERABERABERABERABERABERABERABERABERCBUAmMHTu2UkMJh+onnHCC3XTTTc5clvzb1F2vkJZY3bFWTuVFQJoi5dWeqo0IiEDABNghwo4d7RQJuBOo6kET8PeAoCGo8iIQMAHmALIlHnAHUNWDJqB3gKCbv6iVxywW5rDwB5L8ntmrVy/nzxKfFvgN6devX1HLosRTE9DzPzUXHRWBbAQkFMlGSPEiIAIiUCIEWrRoYcccc4ycrJdIe6mYIlBoAh06dKjcrVfotJWeCIhAvAmwINKxY0dr1KhRvAuq0omACBScADvyO3XqZE2aNCl42kowbAI7duywBQsW2Lp16+ycc86x1q1bVwNyySWXuHdQOVGvhqbODrRq1cqZKjv66KPrLE9lJALlQEBCkXJoRdVBBERABCICCEX4KIiACIRJAIeW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ERABERABERABESgAArt377aPP/7YaQPs3LnTbX5PnjzZrrnmmgqlk7PtCkj0gwiIgAiIgAiIgAiIgAiIgAiUE8i4UARH688884w73Xbw4EE7evSoc7SOZJMFXO3atZ3K//e+9z076aSTyjOqP0RABERABERABERABEQgaAQwizRt2jSbN2+e8xmB6SzmzfiQwGQaWiP9+vULGhaVVwREQAREQAREQAREQAREQAQqTSBjQpHt27fbvffeaxMnTjSEISzo+LCg8wF1b1Q/i4uL7d1337Wf/exndtlll7nf/D36FgEREAERiEyA8ZRxVUEERCCYBPzcKpilL8xS4yPivffes23btpWP78yVqWvmzWeccYbTsi7M0qtUyRIIX1sl+7zuFwERyE8Cfg0QureSnyVRrkVABCpDQGuAylDTMyJglhGhyJ49e+znP/+5/fOf/3TaIJx4q1+/vjvVhh3kmjVrOjNaS5YssaVLlzrtkdLSUkNbpE6dOjZ69GjVlQiIgAiIQBwCLVu2tKZNm1qNGjXi3KnLIiAChUjg5JNPPu6wSSGWMShlKikpsQkTJhhzY4QgoYGFb5cuXez888+3Ro0ahV7S3wEmgPZQz5495VsswG1ARQ8uAUyRY4a8evXqwYWgkotAgAm0adPG2AuQX5EANwIVvVIEMiIUGT9+vP3jH/9wk/QmTZrYHXfcYVdeeWWFRR4l2LJli91///32xhtvuFNwt956qw0ePNgaNGhQqQLqIREQAREICgFMEMqOfFBqW+UUgYoEOGSikL8EOOF7+PBhmzFjhuErBBOzoU6zEY40btzYRo0aZZ06dcrfgirnaSFA+6hVq1Za4lakIiACuU0AoShmyBVEQASCSUAC0WDWu0pddQJpF4ps2rTJEIocOnTInV568sknrWvXrlFz3qJFC3v44YcNDZLHHnvMOCn39NNP2/e///2oz+iCCIiACIiAuXG2rKzM6tWrd9xGmtiIgAgEg8DevXtdQRkDFPKPAAeD/ud//sf5DUHAjUAEQYnf7DrttNPszDPP1Pief1WbkRzTVtC09z4aM5KoEhEBEcgJAgjR9+/f7wSj2hzNiSpRJkQgowRwUcDBGizyhGsYZzQjSkwE8ozAf6Q7v/gSmT9/vlPjwhxWLIFIaF5uu+02ZxqAiT22lBVEQAREQARiE9i6daszQciESEEERCB4BFavXm0rVqwIXsELpMRsZLGoZe7LBjcfTKL07t3bxo0bZ0OGDJFApEDqOh3FwDzxsmXLjANpCiIgAsEigEAEc4u7d+8OVsFVWhEQAUdg8+bNbg6AgFRBBEQgcQJpF4rwgt64caOddNJJ7pN41swGDhzoFoY4mVQQAREQARGITYBTxZwoVhABEQgmAfq/xoD8rfuioiIbOnSoO+mHKbTu3bvb2LFj7aKLLrKGDRvmb8GU84wRoP+HmlzLWMJKSAREIKsEOBmu/p/VKlDiIpBVAtoHyCp+JZ7HBNJuPgtnkJxaxmF6sqqc+BHhBY85GAUREAEREAEREAEREAERyHcCu3btcn7z8LMXHjCRhRmtjh07OrOzaI0oiIAIiIAIiIAIiIAIiIAIiIAIpJZA2oUiLOawbY0q9759+5LK/cqVKw1VcJxKKoiACIiACIiACIiACIhAvhLgkNCcOXNswYIFhg+98847r8KBIebNF154YYXf87XMyrcIiIAIiIAIiIAIiIAIiIAI5CKBtAtF0PbAdBYLwA8//NA4AXfCCfGT3bBhg82ePduOHDlip5xySi6yU55EQAREIKcIoJknO6I5VSXKjAhklAD9n3FAIfcILFq0yGbOnOkOCR04cMB27NjhtEF69OhRIbPJalZXiEA/BJaAxoDAVr0KHnAC+KBS/w94I1DxA02Aw+TaBwh0E1DhK0kgvnSikhH7x1q3bm1nnHGGffbZZ/bUU09Zo0aN7MYbb/SXI35jNuDHP/6xFRcX26FDh+zyyy+PeJ9+FAEREAER+IJAy5YtrWnTplajRo0vftRfIiACgSHQpUuXwJQ1XwrKnPb99993/vXQmMYsLNoge/fudVojbdu2tfr16+dLcZTPHCaAP4FevXrJr1AO15GyJgLpIlC3bl3r06ePtAzTBVjxikCOE2A+yV5AIgfQc7woyp4IZJRA2oUimM46//zzbcKECc5G8gMPPGDvvPOOXXHFFc6ROp2XSTwO2TGX9fbbb9sbb7xRLhA566yzbNiwYRmFosREQAREIB8JcLpYJ4zzseaUZxFIDYFatWqlJiLFUmUC+MObNGmSoSGCZgineHGC6U/zdujQwYYMGeJMzFY5MUUgAp8TQOBWu3ZtsRABEQggAfZT1P8DWPEqsgj8HwHtA6gpiEDlCKRdKEK2hg4darfccovdd999zmk69pTnzZvnFodM4AksEvlg9sE7Z2/fvr09/vjj7kSdu0n/iIAIiIAIRCVw8OBBp12H2UI23xREQASCRaC0tNTNpRgDFLJDgLks2tEffPCBUR8+MN/lGoKr4cOH26mnnqrTfB6OvlNCgPZVUlLi1k116tRJSZyKRAREID8IYHIcbUQEI9IYz486Uy5FIJUEOICD7zq0j7UPkEqyiqvQCWRs12zcuHH2+9//3tq0aeNOMmHzjk7LSTo+/M3LnAk9n1GjRtmbb75prVq1KvQ6UPlEQAREICUEsFGPxh3jqYIIiEDwCKxbt87WrFkTvILnQIk50LNx40Z7+umn7ZVXXjlOIEL2MGeAj7zvfe97NmDAAAlEcqDOCi0LrK2YA2zdurXQiqbyiIAIxCHAhuiKFSucYDTOrbosAiJQgAR49zMHYC6gIAIikDiBjGiK+OxceOGFTtjBYhGTAvPnz3ebdwhBOEFXVFTkFoqXXXaZO0Hnn9O3CIiACIhAYgS89l1id+suERCBQiJA/9cYkPkaxbHlxx9/7Oa2pI4ZEwLzW8wZtGjRws477zzDz56CCKSTgPp/OukqbhHIXQL+/a8xIHfrSDkTgXQS8GNAOtNQ3CJQiATSLhTBUbo/sYTQAydgOE4PdZ6OhghOJxVEQAREQAREQAREQAREIN8IIAhBCIJGCFojCEMaN25sgwYNchoi+VYe5VcEREAEREAEREAEREAEREAECplA2s1nLVy40MaOHWvXXXedzZ49OyJLCUQiYtGPIiACIiACIiACIpAwATblFTJPAEFInz59rF27dobWSNOmTe2MM86wb37zmxKIZL46lKIIiIAIiIAIiIAIBIoAawCtAwJV5SpsigikXVME+5YIRnr37u0cf6Uo34pGBERABEQgjIDUZsOA6L8iEDACOFbUgih9lY6/pm3btjlzWAhCQkO9evXs7LPPtmXLljlTsJjMUhCBTBPQPCDTxJWeCOQOATlXzp26UE5EINMEeP9rDMg0daVXCASOX9GloUQ1atRw5gPq1Klj/K0gAiIgAiKQHgKNGjWyWrVqyRxhevAqVhHIeQL4rJBQJD3VtGHDBpszZ45zZH/xxRdb+/btKyTUuXNn48PCVEEEMk0AE260S623Mk1e6YlA9gkw/+/QoYOx56IgAiIQPAJNmjQxDuh4v3bBI6ASi0DlCKRdKNKgQQPr2LGjbdq0yfbu3Vu5XOopERABERCBuARYCGkxFBeTbhCBgiXQsGHDgi1btgq2b98+mzZtmi1ZssR27dplaItMnjzZrrzyygoCaAlDslVLShcCtD/8NyqIgAgEjwDmyNX/g1fvKrEIeAL4buajIAIikByBtPsUadu2rTMnsG7dOps5c6YdO3YsuRzqbhEQAREQgYQI7N+/33bu3KlxNiFaukkECo/Anj17bPfu3YVXsCyVaMaMGfbMM88Y34ytbDrjQH3x4sXOTJa0crJUMUo2IgHaI+0UQZ6CCIhAsAgcOXLE9f9Dhw4Fq+AqrQiIgCPAu585wL///W8REQERSIJA2jVFOLV86aWXulN1Dz30kNMaGTNmTBJZ1K0iIAIiIAKJEGBDFHv3nBKR6mwixHSPCBQWAbRyWQxhSk+h8gRWr15t7733nu3YscNphiAMwU4zTtQ5jXvOOee4+aw0QyrPWE+mngAHz4qLiw2NMZ0WTT1fxSgCuUzg4MGDxiHUdu3aWc2aNXM5q8qbCIhAGgggEEGjuX79+vItkga+irJwCaRdKAK6vn372uOPP26333673Xjjjfb666/b5Zdf7n6XmmfhNi6VTAREILME2AxlU0SnlzPLXamJQK4Q8GNAruQn3/JRWlpqb7/9tq1cudIJQMg/gg/GVNh27drVCUSw2yxnlvlWu8HIL3MA2qqCCIhA8AhoDRC8OleJRcAT4N2v97+noW8RSJxA2oUi2F0+//zz3alFFpAsLidMmOAWncls3HH6UUEEREAEREAEREAEREAEUkkAsyP4DZk6dWq5Zkho/BzgGTlypHXr1i30Z/0tAiIgAiIgAiIgAiIgAlknIO3lrFeBMpCnBNIuFAnlIsllKA39LQIiIAIiIAIiIAIikE0CGzdudBrMHL454YQT3OEd8sPisnbt2k6r+ayzznLXsplPpS0CIiACIiACIiACIiACkQgkc+A80vP6TQSCSiDtQhFs2vbq1cvq1asXVMYqtwiIgAhkhACbeF4jLyMJKhEREIGcIkD/16IouSpBEIIPplB2zF2xyz5ixAiTmdfkeOru7BKgHfNREAERCB4B3mU6LR68eleJRQACev+rHYhA5QikXSjSr18/mzFjRuVyF/IUi3y95EOA6E8REAERCCPQoEED5wRYTtbDwOi/IhAQAs2aNZNQJMm6bt68ufXu3du2bNli1atXt9atW9vAgQOtc+fOScak20UguwTYEGnVqpWcLGe3GpS6CGSFAM7V6f9oOCqIgAgEj0DDhg3d+1/7AMGre5W4agTSLhSpWva+eFoCkS9Y6C8REAERiEQAjTxp5UUio99EIBgEcACuEJnAjh07nANKBEfhAaHI7t27nVZI//79ddI+HJD+nxcEEIog5FMQAREIHgGE+i1atAhewVViERABR4DDkXwUREAEkiOQN0KR5Iqlu0VABEQgeAQOHjxohw4dchMimc8IXv2rxCJQWlrqNEW0KPqiLTAmLlq0yObPn+/YXHfddV9c/L+/OFl77rnnOjNaFS7qBxHIEwJo1ZeUlDiN0Tp16uRJrpVNERCBVBA4cuSI7du3z2mK1KhRIxVRKg4REIE8InDgwAErKyvTPkAe1ZmymhsEZHQ2N+pBuRABERCBKhPgpPP69euNhZGCCIhA8AjgLBzH4Qr/S2Dp0qX2r3/9yyZMmGBr1qyxdevW2axZsyLikbmBiFj0Yx4ROHbsmJsDoBWlIAIiECwCHADgHYdgREEERCB4BHbu3OnmAP/+97+DV3iVWASqQCAlmiKZ8PeRiTSqwFGPioAIiEDWCbAhcvjwYfkUyHpNKAMikB0CCES1GDLbvn27ffTRR7Zq1Srbv3+/qwyEHrB5//33nQ8RTI0oiEChEWAOwFxAQQREIFgE2CvRHCBYda7SikAogaNHj7oxgLFAQQREIHECKRGK4O/j5ptvtrlz56bN9ACLWwUREAEREIHYBOR/KTYfXRWBQiYQ9P7PhtDkyZNt3rx5hhkBhCAwYYHIZnGbNm2cmSwJRAq5FwS7bEEfA4Jd+yq9CIiACIhAUAnw/tccIKi1r3JXhUBKhCJkAJMNmCaQHfuqVIeeFQEREAEREAEREAERSJbAwoULbeLEibZ3795ybTkWh5yar1mzpo0ePdr69Onj/C0kG7fuFwEREAEREAEREAEREAEREAERKCwCKROKeMlkIkIRr9IVSZLJtfDf/f2FhV6lEQEREAEREAEREAERqAqBrVu32ltvveVsqUeag/bv399GjBhhcjxdFcp6VgREQAREQAREQAREQAREQAQKi0BKhCIILV555ZWYZBYvXmw//OEPbcGCBcYC9aqrrrKBAwc6UwYsYg8ePGgrV660t99+28W1du1aZ5LrrrvushNOSEk2Y+ZPF0VABEQg3wk0bNjQnYLWmJnvNan8i0DlCLRo0aJcS6JyMeTXU2iGTJ061R2mCRWIcLimdevWzlRW27Zt86tQyq0IVJIAfQATcWhGKYiACASLQI0aNYz3nQ4ABKveVVoR8AQaNWpktWrVSps7A5+OvkWg0AikRNoQrtkRDmnWrFl2yy23uFN8v/jFL2zcuHHht7gO3KtXL+Nz3XXX2W233WYPPfSQbd682R5++GHjRa8gAiIgAiIQnUC9evWMj4IIiEAwCTRp0iRQBWfzFyEwvkM4oIMz9caNG9tpp51mAwYMCBQLFVYEEIo0b95cIERABAJIgL0SDkYoiIAIBJNAgwYNjI+CCIhAcgT+I7nbk7+7tLTUXnjhBZszZ47deuut9o1vfCNuJCzqH3zwQScgee6552zSpElxn9ENIiACIhB0AjgS3r9/v9sgDDoLlV8EgkgArVscjAclDBkyxJo2berGvKKiIicI+frXvy6BSFAagMp5HAEEg8wBysrKjvtd/xEBESh8AvjPov8fPXq08AurEoqACFQgwLufMUCuByqg0Q8iEJNA2oUixcXFNnnyZOvQoYMNHjw4YXUuTjr169fP6tevb88++2zMQuiiCIiACIiA2bZt22zZsmV25MgR4RABEQgggTVr1tiqVasKquRogWzcuNGZWY1UsJEjR9qpp55qX/nKV2zUqFFWt27dSLfpNxEoeAJsii5fvtxp2Rd8YVVAERCB4wiwGcoaYM+ePcf9rv+IgAgEg8CWLVvcHECC0WDUt0qZOgIpMZ8VKztoirBIxyxWsjYuO3bs6MwizJ8/P1YSuiYCIiACIvA5AU6GsCmiEyJqDiIQTAL0f4QIhRI2bdpkzAER9PTt29cGDRpUoWidOnUyPgoiIALm5gCFNAaoTkVABBInoDVA4qx0pwgUGgHe/YwBCiIgAskRSLtQhM7JqeW9e/cmrc69fft2t7l36NCh5Eqlu0VABEQgoATi+XgKKBYVWwQCQaBQ+j8HambPnm1Lly41BCOUa8GCBdalSxfnMyQQlalCikAlCBTKGFCJousRERABERABEQgsAd7/mgMEtvpV8CoQSLv5LJx+Yed57dq1tmjRooSzipRzxowZTqAip4EJY9ONIiACIiACIiACIpCXBNBymzlzpr300ks2depUwxQAjtRxIL1hwwbnny4vC6ZMi4AIiIAIiIAIiIAIiIAIiIAI5BSBtAtFcIDZu3dvp8r1yCOP2PTp0xMC8POf/7zcJt4555yT0DO6SQREQASCTIANRZnNCHILUNmDToD+n69jAIdnnnnmGZs4caITgFAOhCHeFACmswYMGBD0Klb5RSAmAfqNTGjGRKSLIlCQBPwaQP2/IKtXhRKBuAR4//s5c9ybdYMIiEA5gbSbz2rfvr1dfPHFNm3aNNu6datde+219rWvfc1943w9PEyZMsX+8Ic/uJOChw8fttq1a9tNN90Ufpv+LwIiIAIiEEagcePGVqtWLatWrVrYFf1XBEQgCATatGmTdxuimFedNGmSLV682GkHU0+o//sNHvzLnXfeeYbWMEISBREQgcgE6B+srdDSVxABEQgWAeb/vC/r1q0brIKrtCIgAo4Ah9Hr1atnX/rSl0REBEQgCQJpF4qQl7FjxzpHmc8//7wdOHDA/vznP9uTTz7pOm3r1q3d5L2kpMQ2b95s3n+Il3Q+8cQTRgdXEAEREAERiE2AhZAWQ7EZ6aoIFDIBBKP5EjjNhqmsyZMn28GDB4+zg4xAhLHsggsusO7du+dLkZRPEcgqAYQiWjNltQqUuAhkjUD16tXV/7NGXwmLQPYJIBDhoyACIpAcgYwIRerXr2/3Gr6aMQAAQABJREFU3Xef8f3iiy/a/v37nXmHPXv2GJ/QwOlAJvWtWrWyBx54wIYOHRp6WX+LgAiIgAhEIeBPVuuESBRA+lkECpwAB0oIua5RsW/fPmcqi8MwaLZ5x5B840Pk9NNPd/M/jWUF3mBVvJQTQNjo11Ipj1wRioAI5CwBvwbg/e/fqTmbWWVMBEQg5QS8+UzNnVOOVhEWOIGMCEVgiErnz372M7vkkkvcQhjfImiNMHnnJc4LnA7cokULGz16tH3961+3OnXqFDh+FU8EREAEUkdg/fr1zjFxnz59rGbNmqmLWDGJgAjkBYEFCxa4eVXfvn1zOr/ezB9zP+aAbOAgHDnppJMMP3L5pPGS06CVuUAROHr0qM2ZM8f1n06dOgWq7CqsCASdQGlpqS1ZssROPPFEZ24y6DxUfhEIGgF88+3YscNOPfVUmdIOWuWrvFUikDGhiM9lr1697MEHH3T/3bBhg+3evdvKysqcmQS0Q9AmURABERABEUiegBcuJ/+knhABESgEAvlyOox8jho1yp566im3cMOUKo7Uu3TpUgjVoDKIQNYI0LeYCyiIgAgEiwCHC9T/g1XnKq0IhBLQPkAoDf0tAokTyLhQJDRrOATloyACIiACIiACIiACIlBYBHbt2uXMpRYVFVUw54Eg5Oyzz3bmsk477TQ5hy6sqldpREAEREAEREAEREAEREAERCCnCaRdKILPkK1bt+r0X043A2VOBERABERABERABFJD4MiRI86Mx+zZs93J1TFjxljDhg2Pi5wTrcOGDTvuN/1HBERABERABERABERABERABERABDJBIO1CkeXLl9sdd9xhI0aMsLPOOsuGDBmSc2rd2OD9+OOPbeHChc4OH04+sWeNTW6cfaY7YEbs5ZdfdsIjVF/r1q1rd999d7qTVfwiIAIFRgAHa/hpUhABEQgmAfp/tseAlStX2ty5c23VqlWGQ3XU+ZcuXWoDBgxwApJg1oxKLQKZIUD/Zy6gIAIiECwC+Oei//OtIAIiEDwC2gcIXp2rxKkhkHahyP79+23KlCnGIvnNN990TjSHDh1qF154obVs2TI1pahCLCtWrLD777/f5s2bZ9u2bTOclLGAx7cJPk4QjPzkJz9xf1chmZiPIhD57W9/a4cPH3b3NWnSREKRmMR0UQREIBIBTNR4B8aRrus3ERCBwiaASdJsbYjg3HHq1Km2Zs0a5y+OuRTO05nbTJs2zTp37myMUQoiIALpIUCf69Chg0zRpQevYhWBnCbA/B8n6/Xq1cvpfCpzIiAC6SHAHiKHq9HEVhABEUicQNqFIk2bNrU+ffrY6tWr3anBdevW2Zw5c+yZZ56xU045xa644gqnPZJ4llN3Z3FxsY0bN84QjBw4cMCaN2/uTjJyymL69OmGlgv55fPXv/7VGGhSHUjn73//u6GtUrNmTRe9/051WopPBESgsAkwEeKjIAIiEEwC2RA6MH+ZPHmyLVq0yAlDEMqwIOMbgQiCmqGfH4Zp0KBBMCtFpRaBDBFAKNKsWbMMpaZkREAEcolA9erV3V5GLuVJeREBEcgcAQ5181EQARFIjkDahSLdunWzSZMmOUHI008/be+//77t3LnTSkpKDLNRH3zwgXuBX3755canUaNGyZWgknejXvb973/fmXTAXNY111xjP/7xj53Naxby5O/mm2+2GTNmODMQt99+u5H/VAZOVSJsQYtm8ODBtnHjRmdCK5VpKC4REIHgEECgy4fT2ZjiUxABEQgWAXx5EBgDMhEwO/rhhx8a/uMQjjDu8GEc4tTqOeec4w7A1K5dW2NSJipEaQSeAIJIhCOsbRREQASCQ4C9Dd7D9H3GAAUREIFgEaD/Mw5oHyBY9a7SVp1A2t+YnBbEPwc+RdCI+PTTT52PETRImLgjfEBT4ze/+Y0NGjTIvvOd7zhTVlUvWuwYJk6c6E42MmnAz8mvf/1rd5qRU9aonXKy8YUXXrDWrVu7xT3aLW+//XbsSJO4yoDFRsKLL77oNGkuvfRSxylbZi+SyLpuFQERyFECmzdvtgULFlhZWVmO5lDZEgERSCcBNFyXLFmSziRc3Jgb/dvf/mavvPKKO+iCEMQLYvnu1auX3XjjjXbmmWdanTp1yq+lPWNKQAQCTIANEQSVaMIriIAIBIsAPrzmz5/v3snBKrlKKwIiAAEOnDMHYC6gIAIikDiBtAtFfFb86UGEDT/60Y/cSxstiaGfm1Twp5l4mb/xxht27rnn2siRI92CGx8fdOxUCwueeOIJly4msb761a9GVDXjlON//ud/OuHN9u3b7bXXXvPFqfL3+vXr7Ve/+pWh6nrllVc6gZAGsCpjVQQiEGgCjJOpHisDDVSFF4E8I5DuMQDhB4db/vu//9uZRA0dbzhkwoEXNG8vu+wyp3mbZ/iUXRHIewLpHgPyHpAKIAIFTCD0nVzAxVTRREAEohDQGBAFjH4WgRgEMiYUiZSHUaNG2XPPPWeffPKJ0xDp2LGjs4ePMGLx4sX2gx/8wHr37m133XWX0x7xApJIcSXz2969e53TT7+ARwATLZx//vkuT2wErF27NiWnrw4ePGiPPvqoM5s1bNgwJ5TxJyyj5UO/i4AIiIAIiIAIiEA2CaD9i71if9CFxRdzKTRsOdBy0003OUev2cyj0hYBERABERABERABERABERABERCBeASyKhTxmWvVqpUTfEyZMsUeeOAB6969u3PSicNx7GOjUYL5rauuuspeeuklpxqGkKKyAefmmK9iId+yZcuYzj+xyXf66ae7+/EBsmzZssomW/7cRx99ZE8++aR16NDBlQltFW8HvPwm/SECIiACIiACIiACOUaAuUufPn3cvAiTo/3793emspgrKYiACIiACIiACIiACIiACIiACIhAPhDIGS98OOlcvXq1c9bJKUSEEQcOHHCLboQGu3btcholH3/8sdMewVb1hRde6GxVJwsawQanHEmjc+fOMR/nPoQ07733nqFhgs3+qgTMcN19992G01E0VC666CIXnTRFqkJVz4qACECAU9sIfBVEQASCSYD+n4oxgIMn2Cbm0ApzpdDAgRW0eJm3nHLKKda2bdvQy/pbBEQgiwTo/zKfkcUKUNIikCUCfg2g/p+lClCyIpBlAqlaA2S5GEpeBDJOIOtCkaVLl9rcuXMN7Q00RRCM8DLHMSfCiH79+rlFN4KM2bNnO18k8+bNc87aMaf1jW98w/nlSIbcunXr3GIeMxAtWrSI+yjaJAwy+/fvN4QaVQk4lF+1apUr2/e///2qRKVnRUAEROA4At6sDb6KFERABIJHAJ8eVd0Q2bp1qy1atMj5DkFLd8CAARVAoi3CR0EERCB3COCjkTULZogVREAEgkWAfo/vVvZQFERABIJHoHHjxsbBJe+vOXgEVGIRqByBrAhFEGZMnTrVCULmzJnjBB1oihCaNWtmgwcPdh9MMnAK0QcEIwhOnn32WedjBH8kZ511lnXt2tXfktB3SUmJu8/bwY71ECchGzRo4G7BETr+QCob3nrrLXvxxRfdZOWGG25Iq93t559/3mbMmGHhZsYQ7nDy87rrrrNDhw65ovBb8+bN3WlQftu5c6cTGvlywqBhw4ZukYX2DvxCN124zmYskzD4cJ04QwPXMbNx+PBhF3/o8/zdqFEjpz3Dc5s2bXKmzfzz/MZ14qc8bNiEBp7HnjlpYIYMraLwcqOZQxm4vnv3bvdNvn3w13mO58mnv0785N23A9IPjd9fJw+0KbSJ+M0H/maiShkImGELN5fGy4u2T7zwg6NPn2f8dVjQV0KvE3+NGjWsqKjIPUP+I10nfQSBpE/5fOB5NtG5Tjrhz3Mf5SJ/fNN/0ZoKzR9xtG7d2pV737597p7Q6zzHZh3phz/Ps6RL+uSD68QRGniO+qOcpM0nNJAWEwGuU3baMGn6QBrknxPPtHEYhF6Ha673AcpHO6WNROsDMKKdbtu27bg2Cgffxql72hBtMLSO/HXGOfpItD4Ay/D4+Y0+4oUykfqAj5+8ZKMPMEmETyH3AcYA+hDjNP04tI0n0wfoQzwf2j74G36MZZV9D9A2GcdpW/RR8uQDfzMG0E6ivQfi9QHaH+MwbZv4iSc0+DaYSB+g/KF9xLdxysDfle0DlJHnOWBBXwsNjE+Mk/Rx+ihjVWgd+Oux3gNsiBIof+jzpBmrD9AvysrKbP369bZw4cLy99jkyZPL64V7yB/tKnwcJ03SiPceiPYe4VnKBx++eQ9yECU0hL4HIr0nYBWvDzDO876J9h7goAzX090HYE0bo9w+8LfvA7QBxtHQPky9+z5A26ENhgaez2Qf4D0U2sdIP/Q9wFwuNHCdPpjOPkAbhxFtIdk+QP6YQ5A/2lr485QltA/QRpmrhPZR7mGOTVzR5kLp7AO0F9Lnm/7Duzy0DZGvqvYB+MA50nuAtBiDGee5Tj8NbSPw8e+ByvYB+jjvoWh9gPj9eoM+Fp4+eeMe/x4IH4f9e4Lf470HIvUBys+HdhGpD5A+71LqIh3vgVT2AeZq1FNoG0+mD/j5eujztJF09gHS8u+BbPUBxmHaUWX7AGMYbTzaeyC0D9AGPV++qR//f9p4PvYB5rFYCansXCiZPkAfDF2Th78HwtfMjGGhfYC5Gu3MM+c6wb8H0tUHGIfhFGmuRB+jDXE9Xh+ItGZmzGQuSTmjzYX8e4D4Gefh5gPpx+oDsOI6Y2G090Bl+wB5IP/kj/gr2wf8e4K2Eb5vRFkZ4ykDaTEXC33P+OuJvgfC1xOUIbQPkH74OOzXA+F9BPbUG/2ecYK88Hf4eoDxhfcg90bqA/ShaGtm8sc8mfUAdVnZPsBchOfD51LkOd56gPTJXyJ9gLkYeYSND6Th50KV7QO0UeaM9AHKEBqS6QPUDXVMnnzg78r0AZ7z82wfl74TI5BRociSJUsMwcDMmTNtxYoVzmk5k04qsFu3bjZmzBgnDOnSpUtEDQ5+58Mg9OMf/9g5Y+dllWxgkkKgI9KZ4gUGBvJIXhmUKhNo7L/+9a/doIkg59prr61MNHGfoUyECRMmHLcx7h9k8MTsBax5iREoFx2PAYiBgUm8j4dy07HhxADKdRbqfvDnOgMq9/ACoWNv2bLluM0k0uAev0jhefLh0+BvBhU6MfFisoPBzgfyx3Xi597wRQa/cTKGdsHLi/R5Cfr4iYcXA4MnvzP40wb8dcrA5IvrpMXLiQHOD57kiYGT+Anh8ZM+GymUjxDKh//zPOnzgiYt0if+0PRhyyKBe1mI0158+jwTep1r3OOv8wx5I344c43B38fPdV58vJz99VChAvHDljjgzsuTj4+fMvA7jPiNdrNx40YXF9cIpMFmGIG4qUPSIhA/z8OA37jO8z5+rtPHyIN/uYVu+nOd37mHdkD61AFp+jISF/y5Tt2GtyHu9W2cPhx+PZE+QNyJ9AH6SLJ9gPwRf6w+QPm5Tl4j9QFM2MCAuqcNhm7oUg++jfN7rD5Ae65MH2ACTfrUMfmj3nygfL4P8jvxh/dByharD4ReD2+jxE/7pY5pE+F9iOt+o4T88d4IFbyRJ/Lu+0B4H6IcjI++DzD5oYy+jXOdNPyGcKQ+wPO+D9CGeT68D5CHaH3Atz++Y/UBnq9sHyB/5JNxnPz5/gUf8urzQPyh4xzXYcGHfhyrD3CdPhj+HoBfMn2AMcTnD/60W/oA4xzjPGNI+HsgtA/QBsmnj4MyMMH37wE/Tvs6In+M87Qj7o30HojXB3z6PB+pD9LGvVCENh7+HvDXyUukPkDeuIcyhT/PM5TNvwd8H/BtGGFIcXGxGz/g5n/nPvygMW/w7wHy78cZfx91wO+J9AGYRusDtA/aINfD3wO+/fk+wHXK5euQeH0fitUHKAdtPNJ7gD7gr4e/p4ifcZj3QKQ+wHO+D3A90nuAeygjjCP1AT/OU65I+SNt6pj2HqkPtGvXrrwPRErft3HSD+8D1KFvo7xn4vWB8D5GntgI8u8Bykeb8IEy+fj5Ozx97oWN7wN+HPZ9kOuJ9AHmQsn0AZ8/4qd/+D5C+oz1vn1xnfGVMhC4RhnD+4DfDKMNU4f+Os+Hvkei9QH4cR/xw8gHng/vA9Qxv/s8woo2BgPGt/A2BHfek76Nh1+n3ikf13k+Wh+gnTKPDX3Xkw+e830g0nuAfHJPrPdAIn2AfhCtD5A+DP16gDKFBtoXY2W0PuD7CM/Bn77s2yD8Qt8DsfoAZYVPaOB5n360PkDeY/WB8D4S+p4gTspGHVMf4X2I66HvAd/HfR65zjwotA/QTn378m2QNsLfvAdog76NEw+/+z4QbS7E8zD1fcjz5VnqP1Yf8O2DvsDzoe8J0icv5J++ShulDYfmjzL6cT5SH6He4R+rD9D+4vUBxir/niBOzzDRPuDbeKQ+StxcJ97wuRr9osPnWpzcQx/w7xnY+MBc2/cB+PE+9PnjHt8HeD58nIYfcx2e5+/w9wzpMw+gDokzUh/w6fN8ePq0AZ6ljRAXcxDqObSNxOsDtPFYfYDxkXZOu/BzIV9+8kTZQq/Tjv118kfb833At0GfP/iF9gH6T2gb5BrjO4wJkfoAdUsbjvYe8H2ANk78kfoADKvaB+AT7T1A+yIftPFI7wH6T6w+QNzUo19vwN0z5m/i9m08Xh8IfU/BlHYDO+IPnWtxzQffBlmPhLdB7qF+aAf+PQCH0PzRB2gj1GdV+wB9LLQPEif17/uAn4v4NsZ13wdgFTrf5xq/0cbpA4RI7wGu+3HevwdCy0fZ/XX6SHgfoH4YR8kT7wHK4PPnEv38H/8eiNUHaAdcZ5zyz1OG0D5A/OTRB6779kEbpw+Rfmgg3lh9gDjivQdoI8RDGw9tYzzL77RvxgLeM6HzXa7T/n0f8NejvQei9QEYxOoDtAF/3eePuqdc1K9CcgTcDvrnlffFqiW55+PezcbTxIkT7fXXXzdMZdFo6Vi8aGn85557rl122WXWt29ftyFGA4sXOMX43e9+12lCjB8/3oYNGxbvkeOuf+Nzk1vvvPOO6+y/+MUv7Iorrjjueuh/QPPKK684zQo6Hunee++9obck9Pdtt93mnMQzSH/wwQcVtEQoE35Sli9f7jgsWLAgoXhDb/rss8/sggsucIPht7/9badFA+fQQHkYxNHCoXMS+I1ORX3wIokk+KHsdG6e4QUTHvx1nud6eJOi3HzorHT+8OCf5zkGHz8wcx+/MeiQPn/zYgoN/MbzPn7yz2+hgWe5h/TJH9+hwV/nOZ6Pdp1nSD80fv725SPf5D88MHhSBgLlD48f9gzwxEX+4BgaEr3OM+Q/0vOkT/6ipe+vR3qe58gf37Qp8sjfocH33UjXw58Pb5dcJ33KSdy+bfr4uU79wZFnw5/nPp7nOs9ShvD8kX/fxmEQeh3u6e4Dvo1T9+noA5SPSQOTu/bt2ztenh/fvo2TfhD7AG2DOqDeq9oH4EcbDG1DMM52H/BtPF19ILSPwSA8+DZO+vAJHSe5119PVx8g/iC/Byg7Jkjh26lTpwrjZGgfoP4YD7kfbRDGDj9u0q6pQ74HDRrk5mcsjvi/fw+oD8SeC8XrA/HmWvSdbM6F4r0n6M/R5kL0Q0IuzoVC+0Aq3gPUc3jI5nuAdrVmzRq3IOawUHgbIq/+PcG9lZkLxXoPMEYwDjHfyIc+wJgX/p5KdK4Ey8r0AR8/6cI/PP1E5/s8xzgcPt+njVNHhEjz+WT6QKQ+Ev4eyGYfoPzh6ZM/yg9Hrvn3mgPyf//E6wP0YeKJNk7nYh8gv+yvrF271jp8LhRhQ49xPF4bj3cdxsQTGmK1YdolY0Cs90Cs50kntA+Qv/D0Q6/nex+I14arer2qfYDnyUN4yHYfoA8zltHeIu0LkT/aWbw+QB+nfOHjsG+j8Z7nufBxnN/8egRukeZCPv5Iz/NMaBtPtA/AgwNOCAk4bM6mOYHnI70nyCPjRqRxnvRhGO06v4euB8LHYdJNdC5U1TYe7/lofYB9H8oBGxjwtw/Ui38P5HofoI2Sfx9C5wD+N33HJ/DFcfz491bqjvnz59vNN9/sBi4aLR0T6erll19uV155pfubTksFJhpotHwideJE4vCDBA2ePMUKfrAiPQYwBohkA9ox7777rsvvT3/60woCkWTjS+T+Xr162fDhwysM8jzrO72fsITGRz34QSz0d/83DPhECzzPIBMtMMjGip+8+fqJFAfXYz1P/PHSZxCPFog/1nWeixU/12Plj+ux4if9WG0s3nXij/V8vPQTeZ7JLp9ooarXaZeR2qZPL178ibTRWG0sn/uAFxjRRqO1Az/R8jzDv9UH4vehqrbReG24qvFnuw/ES1/vgfS9B5jTMEGO18ZYBLz99tvusIpfcFEvzHn4P1q5+BNBS5K4wkNV22i8/FU1/nhtkHE+ne+BRNKPNVdgHI6VP67Her6qc6F47wnaQ9DnQlVto+noAyze/TwgXhtSH6jaeqGqfYD6idWHuB5tHkfa8a5zT6znuR5rPZLI9Wz3gXjpJzIOxxpnq7oeSCT9WON4vD7M9dDn/WEdfick8h6I1QbjvQdIJ9bz5CE0f/w/NMR7nuux2ijX47XxeNdjxU9e412P1warOs7Hiz/e9UTaYKw+EO/5ql6P18eo41j543qsNhavD5B+rDYc73nSj/U8bShW/uI9z/VYbZDroW2c9hb+W+h18hMeYsXPvfGux2uD6gPx907jtZFYfYA2Guv58PrW/yMTiL67Hfn+pH/lBY1qL6cM0Qa5/vrrnXYIlcdAWpmAOtYPf/hDp+GRrD8R0vMqRWwKoFIWL3CCkgGGTh+rUUaKB5Wwxx9/3J3CPP300+073/lOpNscC9JIVfBx+e9Uxat4REAEcp8AG5sKIiACwSOQyDt/2rRpzj8bwg8/VvAccyLmRyNHjnS+2ljIKIiACIiACIiACIiACIiACOQHAT+3z4/cKpcikH0ClZNKJJFvhB9jx461O++809mkTuLRqLeyaP/yl78c9Xq8CyeffLLbCGADINzWZfizDCrYMmTDAMEOWi7JBNRYEQrxPDb/Ro0aVeFxrqH6RzpIrBGk+Puwi/mf//mf1rNnzwrP6QcREAEREAEREAERSIQAc5F//vOf5XOa0Gc49HHqqafa2WefHffUW+hz+lsEREAEREAEREAEREAEREAEREAE8pFA2oUiAwYMsOeeey6n2HTv3t0JRdBiwYdHrIBQZN68eU5YgS8O70w61jPh1zBlQUBAEk1yi2AE9Se+UX+fO3euuxcnS6EOgcPj1v9FQAREQAREQAREwBNgHhEpcMgiXA0eYQjzGvy74UBYQQREIP8JRBsD8r9kKoEIiEAsAur7sejomgiIgAiIgAhUJJB2oUjFJLP/S79+/ZyzcRySYRoLR6MdO3aMmDHMa+EXBYEFtrV79OgR8b5oP2JnsHPnzs7WW7SJCr+jKbJx40ZnyxuzYjhIIrCJkazJrmh50e8iIAKFTQDNPDTp0DhTEAERCB4BDm9EC8w1zjjjDNu8ebPzO9C4cWMbPHiwNFGjAdPvIpBnBOjjzAFkXzrPKk7ZFYEUEGD/gP4fz4dACpJSFCIgAjlIgHc/h7HZt1QQARFInEAghSIsGi666CL761//atu2bbPXX3/dbr311ojUnnzySTe4MMj07t3bCVMi3hjlRwQif/vb36Jc/eLndevW2bXXXuucnqId8t57731xUX+JgAiIQAIEioqKjI+CCIhAMAm0a9fO9u/fb8XFxda2bdsKEDgAgjlOzIEOGjRIAtQKhPSDCOQvAQ5ERDvklb+lUs5FQAQSIcBexUknnZTIrbpHBESgAAk0a9bM+CiIgAgkRyClQpFPPvnEaTwkl4XE7h4xYkRiNyZ417hx4+yll15y/juwsc3mAI7QQ8MHH3xgTz31lOFsFLMSV199dehl9/fhw4ftH//4hzN5xX1Dhw5N2u8IEaG14s1sRTOxVSFx/SACIiACIQQwCYj5PUzkIPxVEAERCA4B5iMrVqxw2q1onjLP4dRoeDj//PPDf9L/RUAECoAA64dDhw45YSem8RREQASCQwBfqWVlZUbfR2tEQQREIFgEWAcwDmgfIFj1rtJWnUBK35i//OUvnQPPqmfr+BiY5C9cuPD4H6v4P8xToR3yq1/9ypYtW+YcwV966aXO1BXCiUWLFtnzzz/v/HkwsWBzAQft4YHFB/Fg/qpu3brOf0qyztjD49T/RUAERKAyBHbt2mWY/OvUqZNbFFUmDj0jAiKQfwTWrFnjhCELFiwoN8PJwY6xY8fmX2GUYxEQgUoRYP2C/8LK+kCsVKJ6SAREICcIsCeB5Qn2ISIdiMiJTCoTIiACaSOwY8cOKykpcfuZEoymDbMiLkACKRWKbNmyxQlFop1QRrhBB+XDPby844V0ak3ccMMNTlPkiSeesFmzZjmn6/gNYVHBKUtOXWNi4vbbb3dCkWh55R60RPD9oQEoGiX9LgIikG4CnBDDdI7XOkt3eopfBEQguwQQhM6YMcMd7uBv7AgzD+Gk2NKlS23Tpk3WqlWr7GZSqYuACGSEAGsm5gDSEskIbiUiAjlFAE3xffv2uf2LnMqYMiMCIpARAn4fIJ37pxkpiBIRgQwTSKlQZMyYMc5xeTShCLZuV61a5bQw6LRXXnllQsVNV8dGs+NHP/qRM5v18ssvu40FTluyocAmAqaw0B457bTTogo7yNuePXucpghCFD6VCUxkSktLbffu3XKQVhmAekYERMAJm6ONv8IjAiJQOASYM0ybNs1ph6AdhhCEORaBa/gmGz58uHwMFU6VqyQikBAB5gCaBySESjeJQEER8H1f/b+gqlWFEYGECfgxIOEHdKMIiIAjkFKhCAKGWCeUa9WqZc8884ytXLnSnWT4zW9+k/VqQLtj1KhRdsYZZ9iBAwecUIMBBcEIQhO0QGIFrs+dO9cQjvBc06ZNY90e9Vr37t1t/PjxLn2/sRH1Zl0QAREQAREQAREIJIHly5cbprFQk+cgBnMPNEQQhqC12rt3b7vgggsMp6tcUxABERABERABERABERABERABERABETieQEqFIggR4oVQxz/YvM2FwKZBgwYN3CfZ/LAR0a5du2Qfq3A/qu6tW7eu8Lt+EAEREIFkCKRLsy6ZPOheERCB1BNAK5XDE2jc+gMozF/o8whE+vXr52yJ16hRw5nzTH0OFKMIiECuE9AcINdrSPkTgfQQ8H3ff6cnFcUqAiKQqwTo++r/uVo7ylcuE0ipUCSXC6q8iYAIiEChE+BkeFFRUbkZnUIvr8onAkEhMHv2bHvrrbcqLHYwm4W5TzReO3ToYMXFxeUCk6CwUTlFQAT+lwBCUuYAaMEriIAIBIsABywbN25sHEBVEAERCB4BDqgjFOHQtoIIiEDiBCQUSZyV7hQBERCBnCbAZggfBREQgcIiEO4snc1PFj8DBw50ftG82c22bdsWVsFVGhEQgYQJMA6ceOKJCd+vG0VABAqHAGbKTzrppMIpkEoiAiKQFAHM+FfWlH9SCelmESgwAhKKFFiFqjgiIALBJcCpcczq4BOJTVMFERCBwiDQsmVLZx5rxowZhunRTp062bBhwyqY/cSMFqfE8C2iIAIiEDwC+BnilKgXlAaPgEosAsEkwLufOQB9XyfFg9kGVOpgE/D7AFoDBLsdqPTJE5BQJHlmekIEREAEcpIAjpd37drlToqhRq8gAiKQPwQQaNKH8QmCn7PwMHz4cMOvyKBBg6xjx47hl93/161b5wSjnTt3jnhdP4qACBQuATZEVq5c6QSn8lNYuPWskolAJAIHDhww5gAcomjUqFGkW/SbCIhAARPYunWrlZSUGGsADkgqiIAIJEZAvSUxTrpLBERABHKeQFlZme3bt08+BXK+ppRBETieAMKQJUuW2Ny5c53AY/To0RW0vTCNcfXVVx//YNj/2BRhY1RBBEQgeAQ4Kb5//37ToYjg1b1KLAJoibAGQFtMQQREIHgE/D4AcwEFERCBxAlIKJI4K90pAiIgAjlNAJNZMpuV01WkzInAcQQQYixcuNAWLFhga9ascdfY2OjataszkXXczQn8R2NAApB0iwgUMAGNAQVcuSqaCMQg4Pu+1gExIOmSCBQwAfq+TOcVcAWraGkjIKFI2tAqYhEQAREQAREQARGITABByPz582316tV2+PBh5weE013btm1zWiMdOnSQ+ntkdPpVBERABERABERABERABERABERABKpEIGVCERbyiZxM8I5/pNZVpXrTwyIgAiJQgQDjKn4JFERABHKXwIYNG+yTTz5xtr/37t3rTnVh+xcNEb7xGdK/f/9Knfai/2sMyN26V85EIN0E6P9aY6WbsuIXgdwj4NcA6v+5VzfKkQhkgoDWAJmgrDQKkUDKhCIIRN58801nyzKacAQbt9OmTXO2Lln4v/jiiwkx5eV+xRVXJHSvbhIBERCBoBKoW7euNWvWzL70pS8FFYHKLQI5SwBb/5MnT3ZaINj9Zm5DX+UbgUi7du1sxIgR1rZtW6c1UpmCNG7cWBuilQGnZ0SgAAhgNqNp06ZWp06dAiiNiiACIpAMAfZZWAPgf0xBBEQgeATq16/vDlTJhFbw6l4lrhqB/8fjny/IU+KNZ9iwYVZcXBxTYwQHoN4BWM2aNRPKPVLPVatWJXRvkG/67LPP7IILLnCnRB977DEbO3ZskHGo7CIQOAIM5Xw0GQpc1avAOUyAPokD9SlTplhpaelxQgvmRLVr17Zzzz3XevXq5TRFoh0sSaSIXktEY0AitHSPCBQeAcYAxpCqjCOFR0UlEoHCJ+DXAOr/hV/XKqEIRCLgxwCtASLR0W8iEJ1AyjRFSKKsrMwOHjyY8EScexMJdHAFERABERCB2AS0EIrNR1dFINMEDh06ZC+99JKtXLmyggYXcxtMZQ0fPtxq1KiRkqxpIZQSjIpEBPKWgMaAvK06ZVwEqkRAa4Aq4dPDIpD3BDQG5H0VqgBZIpBSociZZ55pnTt3TnlRJBRJOVJFKAIiUIAENm/ebDt27LCTTz45ZZusBYhJRRKBjBHAVGirVq1s+fLl5UIRFi0dPneijmZnkyZNUpoX0kH7pFu3bimNV5GJgAjkPgH6/pIlS6xBgwbODF/u51g5FAERSBUBzHKuXr3a2rRpY5jSVBABEQgWAXwW7t69260BWH8oiIAIJEYgZb0FwcUDDzyQWKq6SwREQAREIOUE8EvAyXQJklOOVhGKQKUIsCjp16+fLVq0yPbs2ePsfQ8ZMsR69uxZqfjiPYTGrjehFe9eXRcBESgsArz7mQNgkk9BBEQgWAR49zMHQDiqIAIiEDwCuChgDNA+QPDqXiWuGoGUCUU4+aggAiIgAiKQXQIai7PLX6kHkwAnNHGkjuYHztNDA6e2MZG1adMmO/vssy1Rf2qhcST6t/p/oqR0nwgUJgGNAYVZryqVCIiACIiACMQiwPtfc4BYhHRNBCITSJlQJHL0+lUEREAEREAEREAECpMA2llr1qyxWbNm2ZYtW+zyyy935rJCS4uNf5yo81EQAREQAREQAREQAREQAREQAREQARHIPgEJRbJfB8qBCIiACKSEAOqyMp2TEpSKRATiEiguLrb58+fbwoULrbS01PW9Tz/91EaPHm3VqlWL+3w6bqD/awxIB1nFKQL5QYD+L9MZ+VFXyqUIpJKAXwOo/6eSquISgfwhoDVA/tSVcppbBCQUya36UG5EQAREoNIE6tata02bNq1gvqfSEepBERCBCgRKSkps5syZtnTpUtu6dauhCVK9enVDa2T27Nk2aNAga968eYXnMvEDzlUlFMkEaaUhArlHgLGIOUCdOnVyL3PKkQiIQFoJMA9p1qyZ1apVK63pKHIREIHcJFCvXj23JmEuoCACIpA4AQlFEmelO0VABEQgpwk0bNjQ6tevL6FITteSMpevBDh9OX36dKcdgjAEIQiO1Pkd58atWrVyAhH6YLZCy5YtdUo8W/CVrghkmQAbIW3atJFN8SzXg5IXgWwQwF9Z27Zt3aZoNtJXmiIgAtklUFRUZI0aNdI+QHarQannIQEJRfKw0pRlERABEYhEgA0RnQ6JREa/iUDVCCxbtsymTJliO3bscAIQ+hkO1Y8cOeJOZZ511lnWp08fQ1Mjm04Ow528V63UeloERCDfCCCoVRABEQgeAeYe6v/Bq3eVWAQ8Ae0DeBL6FoHkCGjmnBwv3S0CIiACOUtg06ZNtn37duvatavVqFEjZ/OpjIlAvhDYuXOnTZw40dauXWtlZWUu2yw60A7h0717dxs+fLgzWZELAklMeh07dsx69OiRL4iVTxEQgRQRQHtt8eLF1qBBA2vfvn2KYlU0IiAC+UBg7969tmrVKqctwolxBREQgWARWL9+ve3evdutASQgDVbdq7RVIyChSNX46WkREAERyBkCbIZycp3NWgUREIGqEcCJ+vjx48uFIT42+hfq6aNGjbIuXbr4n3Pim01RxgEFERCBYBJgDqAxIJh1r1IHmwBzE/q//IoFux2o9MElQN9nHaB9gOC2AZW8cgQkFKkcNz0lAiIgAiIgAiJQwAQwhYV/EO9M3ZumGDx4sGEuS0EEREAEREAEREAEREAEREAEREAERCA/CUgokp/1plyLgAiIgAiIgAikkQAOi7t162aY0MJXB2bpzj33XKtbt24aU1XUIiACIiACIiACIiACIiACIiACIiAC6SYgoUi6CSt+ERABEcgQAdRlpTKbIdhKpiAIoGqO83R88GCHPzzgPP3AgQNOIHLSSSeFX865/2sMyLkqUYZEIKMENAZkFLcSE4GcIeD7vtYBOVMlyogIZJSAHwMymqgSE4ECICChSAFUooogAiIgAhCoVauW29jNBYfPqhERyHUCOCNctmyZTZ061TkmvfzyyytkGRNao0ePrvB7rv6AFovsiedq7ShfIpBeApj4w+Rf7dq105uQYhcBEcg5AjhW5nBH9erVcy5vypAIiED6CbAPwByAuYCCCIhA4gQkFEmcle4UAREQgZwm0KRJEysqKrJ8FIrs27fPJk6caIcPH7bTTz/dOnTokNOsM5G5GTNm2KpVq6xnz57Wu3fvTCQZiDRoY0uWLLE5c+bY6tWrXX9ZvHixLV++3E4++eSMMFi0aJF99tln1rFjRxs4cGDK0lS/SRlKRSQCeUcAM3+dO3fOu3wrwyIgAlUnUKdOHdf/tSFadZaKQQTykUDz5s2Nj8aAfKw95TmbBCQUySZ9pS0CIiACOUbg6NGjNn36dPvoo48qnDbjFBon0du2bWt9+/Y1hDCpCnv27LFHHnnESkpK7Je//GVOC0WWLl3qhBWbN282hDlsRDVq1MhtqJ922mkpQbJ//37Hobi42J566qmIceLrYv369e6zYcMGO3jwoDshPHToUOvevXvEZ/jxyJEj9vHHH7t6rlatWtT7atasaWPGjHH1HfWmBC7MnTvXFi5caNQxrFq2bOkEX61atUrg6arfApuZM2ca9YVABHNYu3btcqwoP+rm/E6bT1QogjYGdQP/TZs2WWlpqVFnlK9hw4ZO2EFbiMaXOnjooYdcH3r55ZetXr16VS+oYhABEQg8AcYzbYgEvhkIgAiIgAiIQAAJaA4QwEpXkatMQEKRKiNUBCIgAiKQGwS2b99umAQ68cQTKwg0Es0hQpHZs2fbn//8Z+dn4dChQ+XmeNjw5SQap1A44X7JJZfYeeedl2jUce8jbT5M6FIZ8BnxwgsvGKfzb731VrfxXZlNoxUrVjjBzZYtW4wPG+tssMMFdWUcc/fo0cO+9a1vOT5VKcPf//5327hxo+Pbr1+/ClH94Q9/sFmzZjkhEvlAQMJGPeaeyEcsoQiMP/30U1fHCLqo40gBQQ9pIwSrTCgrK7NHH33UCWDWrl3rBAewQpjGaeavfe1rNnLkyLRu4L322mv20ksvOeEFQhnKjv8QhBBw6tSpkyta//797ZRTTkm4mPC+5ZZbnCCEPkc7oLxoaRE3gh8ELNddd11ELR/Swmn7+PHjjbq+6aabEk471o1wph3kg/+TWOXQNREQgeQJHDt2zAns/RiUfAx6QgREIF8JcDCDQyDM0TmcoSACIhAsAlu3bnVrLdborO8UREAEEiOg3pIYJ90lAiIgAjlPgM1tTqxX1acAp9uZTGG26Stf+YrbaEdQwaYvZof+9a9/OS0DFl/cO3z48CqzYRH3l7/8xeWdDeVUBjasEQJ88MEHbiM+UW2A0DxQfoQUr776quPBJjoftB3YaEcT4Y033nAL0m3bttm9995r7dq1C40i4b/RlvnnP//pNtgRsEQKlOeTTz5xl5o2beqEIAht0O5A8BAvUG/cN2LECPvyl78cUTDBPZVh5dN+4IEH7M0333SCm4svvtgGDBjghAgTJkxw7QcNC2zfn3nmmf6RlH6/++679l//9V9OeIWd7VNPPdW1awRaLBzwJ8LvP/jBD5zgB1u8iQa0ctAYou0jWGnWrJkTjLEpgVbM22+/7TRIKCOaT126dKkQ9Te/+U2bNGmSoSly9dVXR3T0XuGhOD+gucTGqIIIiEDwCPCeYg6gzZDg1b1KLALMRZk/cqBFQQREIHgEWJswB0j14cLgkVSJg0ZAQpGg1bjKKwIiULAE0H6ojAZEJCAIVlhYDRkyxG0Y838mWWiG8Ln55pud+SA2f9EmYHO5KqGqG/Dx0maTCOeTVeHD85deeqndeOON1rp1a7fxRLxwOf/8853WAxvs+Ip45pln7Kc//Wm8bEW8jkCEhS0CiWj24c855xwbNWqU00Jo0aKF04S46qqrIsYX7Uc2z9GWoD7RcIg0ia6sf5p33nnHPvzwQ1eOu+++28aOHes0NEjj7LPPtocfftjeeuste/bZZ10ZESqkMiCc+P3vf+8WB2imDBo0yAmBqH944T9k3bp1TjiDcCgZgQj5pP4nT57sNKe8gAlW9BMEiWjBjBs3zvksQdj3u9/9rkLxqFsEKuQFwcj1119f4Z5kf0jlGJBs2rpfBEQg+wSq8o7Lfu6VAxEQgcoS8O9/jQGVJajnRCC/CfgxIL9LodyLQOYJ/Efmk1SKIiACIiAC+UCAyZXfFOfbbx5jmumyyy5zvinWrFnjNpdzuTxsWlMWNuQRYlRmwcgzOMRG+wFhBWbEMMMEE+JE42Ho5748LrroIufbg41uNEsqExA04ePiwgsvjPo4ApArrrjCafOw6Y+vl0hCjagR/N8FX8f+m3oO/cR7Ptp1hCJoY+B7hrYCH88KDRoEB5jlev/99w0Nl1SHX/ziF4Y5OcqFk3ovECMPCPoefPBB69q1q2vDzz33XNLsiAfhCmZq0M7xghG+EbDQRxBcUY+YtEJIEymgpYNwCo0aBREQAREQAREQAREQAREQAREQAREQgcwQkFAkM5yVigiIgAgUDAE2mLt16+Y2c1HT5RMa8HGBJgAb35zQ58MmPpvP+HWIFLz/DO7FvFJoIL4+ffq4eKZNm+aEDk8//bTzaXL66ac7YcTtt9/uTuWHCgYwJ0Y+EFbMmzfPmb369re/7XxH4NOBDz4tcJadSEBYgAAkWmCjHOfamDBgExy/E8kG/LlQXjQO0D6JFkiL/PgQWm7/W7a+MUtFfSIQQEiEoCA80H7wv0I5MQOGyncqA2arEDYgtCgqKnLCEbRi8PEB1w4dOrg2jLDmvffec/lIZfrE5QVVlBHH6pEC2j7UHX5JMMGmIAIiIAIiIAIiIAIiIAIiIAIiIAIikH4CX+yopD8tpSACIiACIpBGAmyUe62INCbjouYEfqTABjen3//6178avjU4Nc8HjRJ8KyCUQIsiPBAfAodomhz+Gv4gEIAg7EDowMYzQgg2tr/61a/a/Pnzj4sajQ7MgMGFzWccoqNZwYfNcq7BLRWB+BGGeK2LysSL6S0EBPgqyYRdaDbrEVQhiOGD2a6qCiioa+oGHvjxCBXeeM74QUFIwbVFixY5fzX+WlW/8atBmUifdKjz0aNHO/NUmL3yAaEM7YO8JioY88/G+qY9EidaMLRbTMtFc3pKHeNPB38906dPjxVtQtdIL5IQKqGHdZMIiEDeE6D/V+bdk/cFVwFEIOAEmHvS/yPNuQKORsUXgUAQyOQ+QCCAqpCBIRD9yGtgEKigIiACIlAYBNhI55PuwKbvggUL3MYLJ/HZdCbgePrOO+90m+o4lr722mudvxHux78EfhMQWtx3333OfBEb1skENo7x1YEGAukMHjzYmbDihP2f//xn5+Pknnvusddee81Fi3mrb3zjG07DBLNXCGx++9vfOpNOXqjD4tHnP5m8RLoXzYSpU6eWb4SHbsBHuj/SbzjpxjE8Jp7SHVg84xeDD5odbKiTZxzIo+WD0CBZXxvkGbNVe/fudVoajRs3jloMBAKYniouLnaCrag3RrmA8AbhB+2IvPuwYcMGp5mBhkavXr3spptuOu66v4++QhuhDaxatcpOPPFEfymp7x07dhgf2hRaU2jKPP/887Z582Yn8EBYFysgOMJkGn2qqgGTYAoiIALBJMA4iLlABREQgeARYD6OBrSCCIhAMAlglpiPggiIQHIEvthFSO453S0CIiACIpBjBNgE5sNJEb/pX5UsslmMqazQgCbErFmz7I033nAbzR07dizfTP7jH//oNvRxmv3II49Yy5Ytyx9FQIJmAEIJBAdTpkxxG+/JnGhD6IDA4P7773caCD5yNFPY3P7Zz37m/JugbYG5LRiQfzbmEQDAxptT8s+m8puT/t75Nr40omkHREuT8uGHA+0NHHCnM5AWWjZoMaAt0eFzc1KYG0MTB0fv+Li44YYbnLNwBBfJBAQiCCzwuRGrHVIXmK9CQ4X8JBoQsiHEmTNnjhPE4XulZ8+e5Y8TH/dQ38OGDYsoEOFmryVEHhHkVCagjfLkk086IR8cac+0NbgiXLrgggucI/tYceOjBt5olyDwC+9zsZ4NvwZH8hQqJAq/R/8XAREoXAKMfYxp0hYp3DpWyUQgEgHe/cwBmIckM7eOFJd+EwERyD8Cfh9Aa4D8qzvlOLsEJBTJLn+lLgIiIAIpI8CpecwfIXxgY7YqgQUVG/RoXbB5zGKLzfp169Y5nx9sIrORzoY0m9srV650JrLYWEcAEioQ8fkYPny4TZw40W28Ey/PJrMBzCTvjDPOOE4g4uPGqTXCErRJOHGPUMQHv1HMRhETRsoSa7PeP5fMN860EfiQR07rx9MOiBQ3fiXY0CJ/yQpUIsUX7Tc2yzhNiNACLQW+ESqh5bB48WJ7/fXXnWYPJtD4/frrr48WVcTf2dinHAhTYnGmjZIX2gxlTiTgK4Q8IphD+EIcn376qSGcQ8BCoA0QH59Y/cBrifBMVUyGwfKaa65xbZm0Edig/YJQBKEMgbxEY0Fdcx1mmJyj/1Y2oJ1CG8eZvYIIiECwCPCuQ1OOsZDDCQoiIALBIcBcijkA5mFTpQEdHHoqqQjkPwEOV2FCmDWADkbkf32qBJkjIKFI5lgrJREQARFIKwEmQggrEEjE2gxOJBM8j6DjV7/61XGn+BGWYHYIB+WXXHKJnXXWWS46TAahxcHm7ogRI6ImgUmmjz76yDAThZAlWaEIjsyjBRaBCIYwZZTJQHpwYiMc/xA333yzM+mUbB4QSsCEkE6hCMypv/CA5gh1i5YLfltw+o4Gw3nnnZcWdWyEBNEEBeF5o14xvUa7QTCC8IlysAnI//FTkw2zEeQfgZwXfrApwaYkPm7+8pe/OH8pN954ozNFFl4m/3+EjgSEIvThqgSvdSOhSFUo6lkRyE8CvH+ZA6AdKaFIftahci0ClSXAgRT6P3M5CUUqS1HPiUD+EuCwGIIRDldJKJK/9aicZ56AhCKZZ64URUAERCAtBNigTZXKPBu0mHBCu8OfZEdQglYImy3t27c/ThuEzVhOybMYi+VHAq0ETrGycGPyhsmhRANliyUs8GaeWBhmKiDIwEcKmguU5a677rJYgptY+fJaLGxspaoeY6UX7Rr1h2Dna1/7mhMyoYmRjI1ahBUILbzGRrR0EADF0yhB6DF37lwnEEHYwP9phzDi+aKiIhs48P+zdx7wchRXvj4o55yzUEJZiCCBkAQCBCIKDHgRYO+uWSdsvDZ+68XYb9dgMAaMccAGjM0zNg5kBAhEkBBIIBBJEiAUUBbKOQfwm3/JdWmNZu7M3Jm50z391e83d+Z2V/yqq7u6Tp1zhlu3hNaSD9IA8QIXCSnShWD9quI7xeerFw//8qGyZTpL2kJ6MXn00UddnW+77ba013pQMKRrIJ+g60ZsCBCAQDwJ6B4QvKfEkwKthkD8CPh3AMZ//PqeFkNABPT8L+X7I70AgagSQCgS1Z6j3hCAAASKSECLz9J6kMNtLYjr/8omW1rc1mJsJs0PLWjrhU2fqggvwjTZk8klCUTkH0XtueGGG5wQqardIoGShAnKS8KWUoa+ffs64ZXaKHMMuQS1Q0IGCQUqW6CXUEzaRRJseKFCsJzFixfbjBkznOaFhBviov7Xtab40lKSVouEOMHrQpoX4qhQmdaQtE/8dStzE4UMqt+ECRPs4Ycfdlosr7/+etprw/e12pCPcKaQ9ScvCEAAAhCAAAQgAAEIQAACEIBAORNAKFLOvUvbIACB2BGobBE6VxhahNbirr79InO6PLSYq7jyaVLZbneZB/IL0VFW75dA5yc/+YlNmTLFaUTcfvvtlZoNS8cteFxaOGERiqgvJcCSMEyfXELr1q2dRtHy5ctNflIk9EgVpF0kYYeEbsHrS9fIc889ZzLJJs66piUw0LfiDxo0yJmrUr6qY3KQ2rjyU5qlCV8v6YLMbil/ta9Hjx7polX5uExYqc7SSBGLdEFCEY0xtSUdq3Rpk48Xcvwn583/EIBA+AlwDwh/H1FDCEAAAhCAQKEJ6PnPHKDQVMkvDgQQisShl2kjBCAQCwJaCM7k3LpYIDp27Oh2uWtnvnyRDBgwIGVRWqSWhoD8nnhzVykjFvCgFpwVNFH0v/PJXkKdX/ziF/bMM884TQcJR+RHJd+8ZX5LgiIt5le2iJ5P3bNNK+GWFuuldSFhTS6hW8KUlbQ35GPl3XffdU7Q1aZg0HUih+QSoPXr1885dNd5pbn//vudoMJP7MVVprJktu3MM8+07t27pxSG+PxVX2k5ScNFpr/ShXnz5tnOnTudIKIYPjikKaO6q+1BoU9yfZYtW+bi+P5PPp/L/zLflasQK5f8iQsBCISXgO43eq6mEhaHt9bUDAIQKAQBzTU0B9CmFgIEIBA/Anr2l2odIH60aXE5ETh0laKcWkZbIAABCMSMgAQNAwcOrFhgrs7mSwiiBX0t/j7yyCMpi9ZC++zZs50zafmBqK4XN5WjjxaMqmKyK9gYCUTkQPtvf/ubybTUddddZ2eddVbB2iKzVdK6mTt3brDYav/94IMPujapT+UfIzlIYCGBRiqtILVBDts1OZ84cWLKRfoPP/zQPvjgA9cnJ554YoXZKF3D8jmjhX1fhkyyyZn5V7/6Vevdu3dWC34yraV0H330kS1cuDC5+k6DRU7bJRSRk/RU16Iv3wtnDsskw4Fnn33W5asXlMp8sqivVVe1Ld+gPMSfAAEIxI+A7mN6Fld2v4kfFVoMgXgQkOlSadJW5tcvHiRoJQTiSUCa8poDVLYRK55kaDUEKieAUKRyPpyFAAQgEBkCfhG3FBWWRoEEHVrQlnPpl156ye3u9wvnEkY88MADTnNAk7WLLrqo2iZt0hzQLnztoluyZEmFA3DxymXBW9oKDz30kN11111u0f4b3/iGXXjhhW7h27cz+J1L3r7P9EIrhgsWLHCmovzx5G+Vo/r4j4Q1EvqoTK8p4M8F66F0EhTMmTPHcVAcxST92JIAAEAASURBVNdx5aF+0mL+vffe6xbqpZUhoUVy+M53vuNevo855piU/k8kxJC2hhzQ63pQvp6NNDieeOIJk4bEySeffIgwQNeGhEwSoEmg8pe//MXlM3r06Iz+aoJ1vPjii61Zs2bukPy+yOyWb6e+//SnPznGEtzIobzYBcP7779v5557rkmD5O6773as/HmlFyNpusg0lrj5tnmG06dPt3vuucfVWQKiE044wSc/5Fvx1dcSnAwbNuyQc1X5x9ejKmlJAwEIRJ+A7gHBe370W0QLIACBbAkw/rMlRTwIlB8BPft1DyBAAAK5EcB8Vm68iA0BCEAgtATkv0GLydopUgrzGV//+tdNi8nSBvnWt75ln/vc5+zYY491i8YvvviiTZs2zQkmvvCFLziNluSF6GKBlVCka9euTjAi00zaTav/JSTRDn1p12RycK2Jppxl33LLLa49EgaI88yZMw+bgCquTBho175MPuUSJFhq2rSp02SQk3GZ5UoVxHnWrFlOKKLzMtWktmiRXo7f169f7+qp/6+44gpnBsvnI4HId7/7Xachcdxxx5lMn6m+yuOVV16xqVOnOh7ys3H11Vf7ZId8qywx03WWqh/PPvtsx0u+QX70ox+560Jtk7+QSZMmOW7SCrnsssuc0COYec+ePW3IkCEujoQkqfIPxk/1W4Klb37zm/bTn/7U3nnnHfviF79oEpSozi8lBHZqo4Qb//Ef/+HMeyXnoWtDggrFT66DXjgef/xx+973vuf6Z/Dgwa4NSiOzYK+99przNSOmus6uuuqqtAId9bHyk9BuxIgRydXI+X/5SdH1x07xnNGRAAKRJ6B7iUwv6n4i304ECEAgPgSkvbxmzRpr1apVzmZP40OJlkKgfAnoPU7vWXoHSKUBX74tp2UQyI8AQpH8+JEaAhCAQGgIaCKkxXAtNucjFNHudS2mS4tAC6zZBi0gS2hw8803u0VwaQhICKFFbS30S4gwZswY+8pXvnKYiS+VI20ClZu8y0XndFwTvORzwbr59Kp/ctAivbREtEAuzQHlp3y1cCTtj24JPxiVBeW5ePFiJ6yQSSn5qpDwIRUf1VHCEAkDpPWQS9BivhbHpYWghfd0QhEJRO644w7n00T5+0V8CcWefPJJe+yxxxz3LVu2mNouTR4f9MLcq1cv58hc7ZAWhdqna0b9JGGIBBNf+9rXDtHi8On17a8RtT8VA8X5/ve/74QBEiapTn//+99dPdVG8Rk3bpwdffTRinpIUD9LM0TO66XtofpWJYi9xoRMgWmhUPVR36j/2rVrZ6eddppdeeWVKTWWFM9fTxKeBIOu5z59+rg8JRR7/vnnHUOlEUPlf+SRRzrfJ1/60pdMQhMxSiXckcaM2nv88ce7hcxgOVX5Lef1qi9CkarQIw0Eok1A9yDNAXQPQCgS7b6k9hDIlYDmLOvWrXNziVx9weVaFvEhAIHwEZAvSAlGtNkNoUj4+ocahZcAQpHw9g01gwAEIJATAS26anE8n6BJlDQnZFJIvgm04zSXoAXs2267zWkcyGeEtFdULy2CSyshnQN2laPFey3QJy/oasf+JZdc4hbYZZIpXRg7dqwz6TR06NDDokhrQ/4/ZNZIC+S7du1yC0eywaxF7ExBXOVbQ5oN0gCoLGgBXC+k3TIIWtLlIY0GCRFkekr8ggINn0bOySdMmOAW7v2xVN/aOShBhw9qh/rh1ltvdUIe7SqU43sJidQu8dWCvxbpKwsyByWBha4XaY2kCuq3H/zgB/b222877voWd/W1rhMt3MmUl7RCggIDHZe/Dwk0Ro4cmdb0VKoyk4/pupFZKmlkSItCwhyVLU2fVAIZn142ueXUvX///s4RfHBcSXNEWlHyRaL6axFSDCVEVJvFUNebOPsQbJ8/JgGWhFvKT+OtEEH1TCekKkT+5AEBCISbgO4Bqe434a41tYMABPIloHHP+M+XIukhEF0CGv/6ECAAgdwIOCPaiRfo7LcC55Y/sauRgEzWaGesdordeeedztZ9NRZPURCAQIkJrFixwtauXesED1qcjVPQY6ycFoIkTJCZKZl9kimyYCh1W3MtX0IoCcjmz5/vhAfqJwk+5ORcApHzzjvvEOGbBGPSNlL7ZeZLgo1yDL/5zW/s97//vckp/M9//vOCNFHCJLGVdgoBAhCIFwEJffUuIEG6tNUIEIBAfAhol7jmWdqQg6ZYfPqdlkLAE1i6dKnTFJF/ynwsRvj8+IZAXAggSoxLT9NOCECg7AlosVqfOIZyEoio/+TEXYItmcGShkMwlLqt2ZYvh+rSeHnkkUecpoi0XrRgL6GHhPfS1pA2RbLmjUxAyAeNFvWkrVGOQRo6Mo+mHV3p/LZUpd1xvgdUhRdpIFBuBLgHlFuP0h4IZEfAj/24vgdkR4lYEChfAhr7er8iQAACuRHAfFZuvIgNAQhAILQEtLgsc1Cozoa2i7KumHxe/Od//qczPbVo0SLr0KFD1mlLHVEmu1599VWbN2+eczyu+mjHkibr2sksW7cnnXSSE3rIv0hy0DFpTsg8VzamzZLTR+F/+aeRUEjm5Lp3716wKosdL0QFw0lGEIgUAQmsZZ4wbpqikeokKguBIhGQKU69A7BDvEiAyRYCISfg1wGy3bwW8uZQPQhUGwHMZ1Ub6uIXhPms4jOmBAiEmYB24WtBVC9GTIjC3FPZ1U0aE9Kq0AuunNiHPUjoMXfuXCcQ2bBhgxOA+OtQ16Ym6/LDIdNOWrz358LermLUT/2q/tXiZTqfLFUpV0InBd0DCBCAQPwIyLeRNkbgZDV+fU+L403AbzzR2GdzVLyvBVofTwJ+HQDBaDz7n1ZXnQBvzVVnR0oIQAACoSKgFyEWQkLVJXlVRovlhVwwz6syGRJrkX/q1Kn2+uuvVwg7JPTwL+kykzVmzBi3i5mXdXPCkGLs5kYYkuFC5TQEypwAiyFl3sE0DwJpCGjOxfhPA4fDEIgBAdYBYtDJNLEoBPApUhSsZAoBCECg+gls3LjRZJZHO0UJEKhOAlrg79Sp0yGmm7RjSaayrrrqKjv//POtcePG7F4scqesWLHCli1bVuRSyB4CEAgjAd1zNQdYt25dGKtHnSAAgSISkOnSjz76yORwnQABCMSPwPr1690cQHMBAgQgkD0BhCLZsyImBCAAgVAT2LVrl0kwwmQo1N1UtpXr1q2b9e/f311/EoCMHz/errzySmvfvn3ZtjlsDduyZYvpQ4AABOJHQJp5mzZtsh07dsSv8bQYAjEnIJOcGv/S3CVAAALxI6Bnv+4B+BaMX9/T4vwIYD4rP36khgAEIBAqAnH20xCqjijTyuilW/5C2rZte5ipNglCjj76aGvevLlzpJ7KiXqZYglNsxj/oekKKgKBkhDgHlAS7BQKgZIT8GPff5e8QlQAAhCoVgIa+4z/akVOYWVCAKFImXQkzYAABCAAAQgUi4C0j9auXWtvvPGGvfnmm3bFFVdYnz59Diuud+/epg8BAhCAAAQgAAEIQAACEIAABCAAAQiElQBCkbD2DPWCAAQgkCMBmc7QhwCBQhKQffr333/fCUO2bt3q/II899xz1rlzZ0MbpJCk88+Le0D+DMkBAlEmwD0gyr1H3SFQdQJ+7PMeUHWGpIRAlAn4e0CU20DdIVAKAghFSkGdMiEAAQgUgUDdunWtUaNGOLMuAts4ZinbtHPnzrU5c+aYHHjXrFnTateu7WzVeofeffv2jSOa0LZZQipsCYe2e6gYBIpKQGYzGjZsaPXq1StqOWQOAQiEj0CtWrXcO4DmaQQIQCB+BLQOoDkAJrTi1/e0OD8CCEXy40dqCEAAAqEh0KJFC2vSpIlbuA5NpahIJAm8++67Nnv2bFu2bJnt37/fXVPagSQHnu3atbOhQ4damzZtItm2cq50x44dy7l5tA0CEKiEQI0aNaxbt26H+XuqJAmnIACBMiEgYWj37t15ByiT/qQZEMiVQKtWrZxfR21iI0AAAtkTQCiSPStiQgACEAg1Ae0OY4dYqLso9JVbvHixvfbaa7Zq1Srbvn27W1zT7kMJRvTCPXz4cBs0aJC1b9+enUgh7E12iIewU6gSBKqJgHaH1q9fv5pKoxgIQCBMBLQQyvgPU49QFwhUL4E6depUb4GUBoEyIYBQpEw6kmZAAAIQ2Lhxo23ZssW6dOmCcITLIScCum6mTp1qEopIGKIgYYhMMcnJupyqjxw50qSJgOAtJ7TVGnn58uXOr1DXrl2rtVwKgwAESk9A92pp98l8Rtu2bUtfIWoAAQhUG4Fdu3bZxx9/7LR4pTVOgAAE4kVAPiD1DofGaLz6ndbmTwChSP4MyQECEIBAKAjohWjz5s0sXIeiN6JTiUWLFtnkyZNtw4YNrtLeFq0W2Jo1a2ann366HXXUUQhDItCl27Ztw6dIBPqJKkKgGARk4lBzAAIEIBA/AgcOHHAbo5o2bRq/xtNiCEDAtA6gTW7a0IYJLS4ICGRPAKFI9qyICQEIQAACEAgHAS186cU3YUM+67B+vVnr1odFl2aRTC5oEi2b9D6MGjXKRo8ejTDEA4nItxZGCRCAAAQgAAEIxIsAz/949TethQAEIACB/Al8tvqRf17kAAEIQAACEIBAsQkkTCTZiBFmX/ua2Y4d2ZX2y19aQoXIbObMw+LLBu2xxx7rTK5IS6Rnz5529dVX22mnnYZA5DBaHIAABCAAAQhAAAIQgAAEIAABCEAg6gTQFIl6D1J/CEAAAhCID4GEzw/7whfM5s07+JFmx623mjVqlJ7BL35h9p//6c7/49RT7YhnnjFLaIEEg5ynyxa9fIf07t07eIrfESLALtEIdRZVhQAEIAABCEAAAhCAQAEI6B2A94ACgCSL2BFAKBK7LqfBEIBAuRKQY+y6deua9wlRru2Mdbvuv99sxoyDCBJaHXbXXZawe3VQMJLsWDPhE8R+/Wuz//N/KpAdkbA3u//b37bab71Vccz/OPfcc/1PviNKQONfZtAIEIBA/Ajo2a97QO3atePXeFoMgZgTkPlTjX98CcT8QqD5sSWgZ3+9evVYB4jtFUDDq0oAoUhVyZEOAhCAQMgItGrVyjnGljkkQpkS+MpXzJYuNZNwxPuOuOces4SDTbvtNrPmzQ82PPH/P371KzviBz8w27+/AsaaDh3so3/7N0sY3yKUIQH5hyFAAALxJKBF0R49erAoGs/up9UxJyDfcDJ/yjtAzC8Emh9bAm3atLGWLVsyB4jtFUDDq0oAoUhVyZEOAhCAQMgIaIcIO0RD1imFrk779ge1QpTvH//4We5/+IOZNEPuuMP2JzRGPkkISOrddJNZQjPEh5UJnyKPn3WW7alZ047ZvdvqJV6gCeVFQDvECBCAQDwJSFNEC6MECEAgfgSkIdKgQYP4NZwWQwACjgACUS4ECFSNAEKRqnEjFQQgAIHQEdi8ebNt377dOiS0AWRKi1CmBFq3NvvZz8zkT+S++z5rZEJI8ummTbY/oTFU66GHDjphl4mthEbJkoRA5Mmzz7bmCefpY/r3tyOUllB2BFavXu3MZ3VM9DcBAhCIFwGZzlu1apVbGNVuUQIEIBAfAnv27LF169ZZixYtEm7mKvEzFx8ktBQCsSKwcePGxF64XW4dADN6sep6GpsnAVbN8gRIcghAAAJhISCByJo1a0zqswhFwtIrRaqHFrxuv90SOtJm995bUcgRTz5p0hVwIo9/CkTmtW1rH3z1qzZy/HjrkTCv1CTZ90hFan5EncD69esTCkOfGEKRqPck9YdA7gQkFNEcQIuiCEVy50cKCESZwN69e00bI6QxilAkyj1J3SFQNQJbt241CUbatWuHCa2qISRVTAkgFIlpx9NsCECg/AjIdIZsihNiQqBZM7Of/9wSnjXN7rzTNTqhF2L6uJDQEFncrZsTnpxyyinWQvEJZU1A4/8f3tdMWbeUxkEAAqkI6B6guQABAhCIFwH/DsD4j1e/01oIeAJ6/rMO4GnwDYHsCSAUyZ4VMSEAAQhAAAKhIrAzMQH+a79+dlmdOlZ/375D6pbwMGId77rLap1xhiX0SQgQgAAEIAABCEAAAhCAAAQgAAEIQAACCQJsKeYygAAEIAABCESUwFtz51rHp58+TCCi5kgQUvfhh63mjh0RbR3VhgAEIAABCEAAAhCAAAQgAAEIQAAChSeApkjhmZIjBCAAgZIQkMo8avMlQV+yQkfOmmVHTJqUvnz5G6mVeNTfdptZw4bp43GmLAhwDyiLbqQREKgyAe4BVUZHQghEngDvAJHvQhoAgbwIcA/ICx+JY0oAoUhMO55mQwAC5Uegffv2zsl67dq1y69xtOhwAr/6lR1x9dWHHk9cA9a0qdnChZbwuJ1wMJKwLZ8woeW+b70VwcihtMruv969e5ddm2gQBCCQHYFaCQH4gAEDsCmeHS5iQaCsCMi5+qBBgxL7YFjeKauOpTEQyJJAp06drEOHDtwDsuRFNAh4ApjP8iT4hgAEIBBxAjVr1rQ6Cd8S7BKJeEemqf4nCSHHP3Tu00/NfvlLs+9859CYQ4aYPf+82bvvml1++cFzcrotwchvf2v23e+abd9+aBr+KysCEogiFC2rLqUxEMiJgOYALIrmhIzIECgLAnKwrPGPo+Wy6E4aAYGcCejZzzpAzthIAAFjKwEXAQQgAIEyIbA9seC9a9cua9WqlUlAQigfAqtXr7ZFixdbz8QuoPYTJ5pde63ZgQOfNfDoo83+8Aez/v0PHpNWiAQif/rTwW8dlcaI0vz0p2YtWnyWll9lQ2Djxo2Jbv+HuweUTaNoCAQgkBWBTxMC8w0bNljdunUTCoMJjUECBCAQGwL79u2zzZs3W5MmTax+/fqxaTcNhQAEDhLYtm2b7dmzx70DIBzlqoBA9gQQimTPipgQgAAEQk1g06ZNtmbNGrcYglAk1F2VU+UWJkxhvfTSS/bRsmV2/rp11v7BBy0h/TqoASLBx3HHmf3ud2aDBx8UgEgzpHVrMwlGFO6//+C3/srHiMxqSYBCKDsCK1euTHTvJwhFyq5naRAEMhOQUGTp0qUJmXcLhCKZcREDAmVFYPfu3bZkyRI78sgjEYqUVc/SGAhkR0CbIvRp3rw5GmPZISMWBBwBhCJcCBCAAATKhIB2hSAMKZPO/Gcz3njjDZsxY4ZtSGgA1EuYRViU2AXYe+9ea6nzEoiceKLZ3XdbwpD8wRQSiPjQpo3Zz35miZmx2f/7f/6oWdeun/3mV1kRYPyXVXfSGAjkTED3AHaI5oyNBBCIPAGZztX4x4Ru5LuSBkCgSgRYB6gSNhJBAPNZXAMQgAAEIACBsBHQbv/Jkyfb7NmzTbv/6iR8Rezfv9/WDBtmW085xVr+3/9r1rfvQYFIv37pq58wpVYhGJF2yM9/bvbNb6aPzxkIQAACEIAABCAAAQhAAAIQgAAEIFDmBNAUKfMOpnkQgAAEIBAtAvIN88QTT9jihA8RmUPRzr8DCV8grVq2tDPHjbMuPXse1Azp0sWsW7fMjZP/EJnSuvBCs7FjLZFh5jTEgAAEIAABCEAAAhCAAAQgAAEIQAACZUoAoUiZdizNggAE4kdAC+jSMCBEl4B8QkxMOFJfv369a4TMIKhfOyUcrJ9//vnWrl27g40bNSq3RkowcvbZuaUhduQIaPxzD4hct1FhCBSMgMa/nhkECEAgXgT+kTCpyviPV5/TWggECbAOEKTBbwhkTwChSPasiAkBCEAg1AS6JDQHOnfujD3xUPdS+srNnTvXnn/+eZOmSDD0TGiGXHTRRTjODELhd0oCAwcOTHmcgxCAQPkTqFWrlh177LHl31BaCAEIHEagSZMmdvzxx+NT5DAyHIBAPAh07949YUCgG+sA8ehuWllAAghFCgiTrCAAAQiUkoC0CnCwWMoeqFrZ2t336quv2rRp02zfvn0Vfai+HDp0qJ1zzjlVy5hUsSOAg+XYdTkNhsAhBLgHHIKDfyAQGwK8A8Smq2koBFIS4B6QEgsHIZCRAEKRjIiIAAEIQCAaBHbs2OGccrdImEqSHwpC+AlICCJhyPTp050wRBNaCUlqJxyrn3766W7XX/hbQQ3DQmDTpk3u+mmZ8D9DgAAE4kVApjM2btxodevWNe0aJ0AAAvEhoPnkli1brHHjxmgWx6fbaSkEKgjI0sCePXtM7wBskKjAwg8IZCRQI2MMIkAAAhCAQCQIaEF0yZIltn///kjUl0qaSZD18ccfV6CQQKRZs2Y2YcIEBCIVVPiRLQH5pFm+fHm20YkHAQiUEQEJRZYuXVrhk6qMmkZTIACBDAR2797t3gGSTbBmSMZpCECgTAjIH6XWAfAtWCYdSjOqjQBCkWpDTUEQgAAEiktAWgbsDCku40LnLq2esWPHWtu2bV3WXbt2tSuuuMJkF5YAgVwJaPxzD8iVGvEhUD4ENP41FyBAAALxIuDfARj/8ep3WgsBT0DPfyxFeBp8QyB7ApjPyp4VMSEAAQhAAAIFJ9C+fXs7+eSTbcWKFTZixAhr1KhRwcsgQwhAAAIQgAAEIAABCEAAAhCAAAQgAIGDBBCKcCVAAAIQKCMCMr9ECCeBXbt2OVvvqXbx9O3b1/r16xfOilOryBBg/Eemq6goBIpCgHtAUbCSKQQgAAEIQCDUBPT8Zw4Q6i6iciElgPmskHYM1YIABCCQKwGpzGvBHdX5XMkVP/7mzZtt4sSJNnny5JSF0WcpsXAwRwIa/5jPyhEa0SFQRgS4B5RRZ9IUCORIgHeAHIERHQJlRADzWWXUmTSlWgmgKVKtuCkMAhCAQPEItGnTxuSjonbt2sUrhJxzJrBs2TJ74YUXnHksTVhlHmvUqFE550MCCGQigC+aTIQ4D4HyJaAF0T59+mBTvHy7mJZBIC2Bhg0buvFft27dtHE4AQEIlC+Bdu3aWevWra1WLZZ4y7eXaVkxCDBiikGVPCEAAQiUgECdOnVMH0J4CLz33ns2ZcoU27Rpk9PgkVrztGnTrHPnzjhTD083lU1N6tevXzZtoSEQgEBuBKRx2KBBg9wSERsCECgLAhKKSjBCgAAE4kkAgWg8+51W508A81n5MyQHCEAAAqEgsH37dluzZo0dOHAgFPWJcyUk/Jg5c6Y9+eSTFQIR8di/f78NHz4cgUicL44itn3Dhg22bt26IpZA1hCAQFgJfPrpp7Z27VrbsmVLWKtIvSAAgSIR2Lt3r3sHkP86AgQgED8CW7dudfcAzQUIEIBA9gQQimTPipgQgAAEQk1ACyErV65EKFLiXtKL6fPPP2+TJk2yffv2Vfh4kaDk4osvttNPP73ENaT4ciUgoejq1avLtXm0CwIQqISAFkI0B5BmIgECEIgXAc09V6xYYTt37oxXw2ktBCDgCMh/5apVq+yTTz6BCAQgkAMBzGflAIuoEIAABCAAgcoISFvn6aeftjlz5phXY5YwRCYNLrnkEuvatWtlyTkHAQhAAAIQgAAEIAABCEAAAhCAAAQgUGQCCEWKDJjsIQABCEAgHgRktuSRRx6xjz/+2AlEJAyRjfdOnTrZ+PHjrVWrVvEAQSshAAEIQAACEIAABCAAAQhAAAIQgECICSAUCXHnUDUIQAACuRLQQjyhegnIh8tHH31kjz32mMmWc+3atU1mTGrVqmX9+vWzsWPHWuPGjau3UpQWSwKM/1h2O42GQAUB7gEVKPgBAQhAAAIQiA0BPf+ZA8Smu2loAQkgFCkgTLKCAAQgUEoCNWvWtDp16lT4sChlXeJU9nvvvWdPPPGEE4SoDyQQqVevng0bNsxGjBhRYUYrTkxoa2kIeIFcaUqnVAhAoNQENAfQc4gAAQjEi4A0kzUHqFEDl7Hx6nlaC4GDBLQZT/cA3QsIEIBA9gQQimTPipgQgAAEQk2gefPmVr9+fTchCnVFy6xyWoTSR04uJRBp2rSpnXLKKTZkyBAmpmXW12FvTocOHdglFvZOon4QKBIBCUO6dOnCHKBIfMkWAmEmoM048lvXoEGDMFeTukEAAkUi0LJlS2eZAMFokQCTbdkSQChStl1LwyAAgbgRkEBEH0L1EpCJLAlEZD6rY8eOzlxW9+7dq7cSlAaBBIEmTZrAAQIQiCkB7Q5t1qxZTFtPsyEQbwLaIa7NUQQIQCCeBCQQRSgaz76n1fkRQCiSHz9SQwACEAgNge3btzufFnLojfmM6u0WrxUioUjr1q2rt3BKg8A/CWzYsMFpinANcklAIH4EpKm4fv16Z75RGosECEAgPgT27dtnmzZtcpsjWBiNT7/TUgh4Alu3brU9e/a491C0RTwVviGQmQBGJzMzIgYEIACBSBDQZGjVqlUmx9+EwhP45JNP3IJTqpy1Q3fw4MEIRFLB4Vi1EVi3bp2tWbOm2sqjIAhAIDwEJBT5+OOPbfPmzeGpFDWBAASqhYAWQzX+d+3aVS3lUQgEIBAuAlu2bHH3AL2vEiAAgewJIBTJnhUxIQABCISawD/+8Q/n00LfhMIS0ATzueees/vuu8/mzJmTMnMc26XEwsFqJKBFUX0IEIBAPAlwD4hnv9NqCIiA5qq8A3AtQCCeBHj+x7PfaXX+BDCflT9DcoAABCAAgTImoN13Dz30kC1atMi18uWXX3Z22+XQlgABCEAAAhCAAAQgAAEIQAACECgVATbnlYo85UadAEKRqPcg9YcABCAAgaIRkH3mP//5z7Zx40bz9lllnkjaIm3btrW6desWrWwyhgAEIAABCEAAAhCAAAQgAAEIVEYALbHK6HAOAukJIBRJz4YzEIAABCJFQIv2crDOTpH8u00qyIsXL7a///3vtn//ficQ0WRTjAcNGmQjR45EIJI/ZnIoMAEvuCtwtmQHAQhEhIDmANwHItJZVBMCBSbAO0CBgZIdBCJEQM9+nv8R6jCqGhoCCEVC0xVUBAIQgEB+BJo3b2716tWzWrW4tedDct++ffbWW285HyIShEjIpG9phQwdOtTGjBljtWvXzqcI0kKgKAQ6dOiAPfGikCVTCISfgBZEZdaxTp064a8sNYQABApKoH79+ta1a1dr2LBhQfMlMwhAIBoEWrZsaY0bN3YbJKNRY2oJgXAQYOUsHP1ALSAAAQjkTaBBgwamD6HqBLZt22byGfLmm2+6TCQQkdZIs2bN7IQTTrDhw4dXPXNSQqDIBHSdEiAAgXgS0POqRYsW8Ww8rYZAzAlos44WRQkQgEA8CTRq1CieDafVEMiTAEKRPAGSHAIQgEBYCOzcudPkFFwLo9oxSsiNwOrVq+3FF1+0hQsXOu0QLTDJdJZ23o4ePdr69OmTW4bEhkA1E9iyZYvTFJHWGAECEIgXAWk0yg+WtBpZHIlX39NaCGi+qo090hSR1jgBAhCIF4EdO3bY3r17Te8AmNGKV9/T2vwIIBTJjx+pIQABCISGwObNm23t2rXWv39/hCI59Monn3xiixYtsilTppgEI5pISiCiieWQIUNs1KhR1q5duxxyJCoESkNg1apVpusZoUhp+FMqBEpJQGN/2bJlbvwjFCllT1A2BKqfwO7du23p0qVuIw9CkernT4kQKDWBDRs2uI0RTZo0QShS6s6g/EgRQCgSqe6ishCAAATSE9AuUX0IuRHQ7voXXnjBCURkfkDmsrS4dOqpp9qwYcPYcZsbTmKXkICuXe4BJewAioZAiQkwDyhxB1A8BEpIQHMAAgQgEE8Cev5zD4hn39Pq/AggFMmPH6khAAEIQCDiBJo2bWoDBgxwWjYHDhxwpkfGjRtnAwcOxGFtxPs2btWXhhMBAhCAAAQgAAEIQAACEIgPAb0D8B4Qn/6mpYUjgFCkcCzJCQIQgAAEIkigVq1aTiNk48aNtnjxYjv//POtR48eqB5HsC+pMgQgAAEIQAACEIAABCAAAQhAAAIQyEQAoUgmQpyHAAQgECECmM6pWmfJ/vLYsWOd6aHGjRtXLRNSQaDEBBj/Je4AiodAiQlwDyhxB1A8BEpEQGPff0pUBYqFAARKSIDxX0L4FB1pAghFIt19VB4CEIDAZwRatWplWtCvU6fOZwf5dQiBefPmWadOnRynQ04k/sExbTIR/o8aga5du+JTJGqdRn0hUCACNWvWtJ49e5p8YxEgAIF4EWjQoIH16tXL9E2AAATiR6Bt27bWvHlz01yAAAEIZE8AoUj2rIgJAQhAINQE6tevb/oQDicgx3MzZ860559/3tq0aWNf/OIXeXE8HBNHIk6gSZMmEW8B1YcABKpKQLbEmzVrVtXkpIMABCJMQMJQLYgSIACBeBKQQBShaDz7nlbnR6BGfslJDQEIQAACYSGwb98+27Vrl0kAQPiMwM6dO+2ZZ55xAhGpFq9atcqefPJJ++STTz6LxC8IlAGB3bt3mz4ECEAgfgT0fNMcYO/evfFrPC2GQMwJaE6r+e6BAwdiToLmQyCeBPw6gOYCBAhAIHsCCEWyZ0VMCEAAAqEmsHr1apszZ45pUkQ4SGDdunVOAPL66687s0LaSavP2rVrbceOHWCCQFkRmD9/vslEHAECEIgfAS2Kzp0711asWBG/xtNiCMScgOa0egfYuHFjzEnQfAjEk4Ce/ZoDIBiNZ//T6qoTwHxW1dmREgIQgECoCNSoUcNq1eK27jtl0aJFNnXqVFu2bJmzsa6dM5ooHnXUUXbqqada06ZNfVS+IVAWBDT+0YAqi66kERCoEgHdAzQXIEAAAvEioA0/jP949TmthUCQgHyJsA4QJMJvCGRHgNWz7DgRCwIQgAAEIkRg1qxZNmPGDFu/fr1zPK+FYk0WTzjhBBs1ahRO1SPUl1QVAhCAAAQgAAEIQAACEIAABCAAAQgUkgBCkULSJC8IQAACECgpAWmDTJ482d59911nW71OnTpOO6Rhw4Y2evRoO+aYY5yQpKSVpHAIQAACEIAABCAAAQhAAAIQgAAEIACBkhFAKFIy9BQMAQhAoLAE5GA9zqZz5GD20UcftSVLljgO0gzZv3+/tWrVys444wzr06cPZkUKe8mRW8gIaPzH+R4Qsu6gOhCodgIa/5oLECAAgXgR0KYgxn+8+pzWQiBIIO7rAEEW/IZALgQQiuRCi7gQgAAEQkygbdu21qxZs1hqQqxatcoef/xx27Bhg+sh2VbWy2GXLl3snHPOsQ4dOoS456gaBApDoEePHqaFEQIEIBA/AtoIIJ9ZtWvXjl/jaTEEYk5AGtF9+/a1+vXrx5wEzYdAPAm0b9/ebQTEr0g8+59WV50AQpGqsyMlBCAAgVARqFevnukTxzB9+nTbuHHjIU3v3bu3XXDBBaYXRQIE4kCgUaNGcWgmbYQABFIQ0GaAJk2apDjDIQhAoNwJaCG0adOm5d5M2gcBCKQhIIEoQtE0cDgMgUoI1KjkHKcgAAEIQCBCBGQqSiak4rhTXMKP5s2bu96qUaOGHX300Xb55ZcjEInQ9UtV8yewZ88e2717d/4ZkQMEIBA5Anr2a/zv27cvcnWnwhCAQH4EpB2td4ADBw7klxGpIQCBSBLQsz+u6wCR7DAqHRoCCEVC0xVUBAIQgEB+BFavXm3vvfee7d27N7+MIphaDtUnTJjghCBjxoyx8847L4KtoMoQyI/AggUL7MMPP8wvE1JDAAKRJKBFUc0BVqxYEcn6U2kIQKDqBHbs2GFz5861TZs2VT0TUkIAApElsHLlSnv//fcRjEa2B6l4qQhgPqtU5CkXAhCAQIEJyHSGtCTKPUgjJpXN9JYtW9pVV12F6nC5XwC0Ly0Bjf84aoqlBcIJCMSMgO4BmgsQIACBeBHw7wCM/3j1O62FgCeg538c1gF8e/mGQKEIlP/qWaFIkQ8EIAABCJScwNKlS23GjBm2c+fOlHXBlmpKLByEAAQgAAEIQAACEIAABCAAAQhAAAIQ+CcBNEW4FCAAAQhAIBIEZs+ebVOmTLHNmzc7E2GnnnqqybEkAQIQgAAEIAABCEAAAhCAAAQgAAEIQAAC2RJgNSlbUsSDAAQgEHICn376qcmmeLkFmcuaPn26vfbaayZH0jINMGvWLOdY/fjjjy+35tIeCFSZgMZ/Od4DqgyEhBCIGQGNf80FCBCAQLwIyHQm4z9efU5rIRAkoPF/4MCB4CF+QwACWRBAKJIFJKJAAAIQiAKB1q1bW6NGjVL624hC/VPVcdu2bfbcc885x3F64atZs6ab8DVo0MCaNm2aKgnHIBBbAl27dsWnSGx7n4bHnYCejz179rQ6derEHQXth0DsCGhe3KtXL2vYsGHs2k6DIQABs7Zt27oNg5oLECAAgewJIBTJnhUxIQABCISagF6I9CmXsGLFCicQ0be0Q/TRLphOnTrZ2WefbR07diyXptIOCBSEQLNmzQqSD5lAAALRI6BnZIsWLaJXcWoMAQjkTaB27drWsmXLvPMhAwhAIJoEtDGSAAEI5E4AoUjuzEgBAQhAIJQE9u7da/v27XO7xGrUqBHKOmZbqffee8/5D9m0aZMThiidTIL07t3bzj//fGvcuHG2WREPArEhsHPnTqcpwotRbLqchkKggoC0KXfs2OF8bdWvX7/iOD8gAIHyJyCzObt377Z69eqVlcZ4+fccLYRAYQjIxLRMTusdQJskCBCAQHYEor1qll0biQUBCEAgFgTWr19vCxcudBOiKDdYvkOeeuop51DdT+qkITJ8+HCbMGECApEody51LyqBpUuX2pIlS4paBplDAALhJKDn5KJFi2zNmjXhrCC1ggAEikZg165dtmDBAtuyZUvRyiBjCEAgvATWrl3r5gD4FQlvH1GzcBJAUySc/UKtIAABCORMQLtEpU2h7ygGabpMmzbNXn311cN2uJx33nl27LHHRrFZ1BkC1UbA3wOqrUAKggAEQkUgynOAUIGkMhCIIAGNfwIEIBBPArwDxLPfaXX+BBCK5M+QHCAAAQhAIE8CMvszadIke+edd6xu3boVuckEyOc//3mTA2kCBCAAAQhAAAIQgAAEIAABCEAAAhCAAATyJYBQJF+CpIcABCAAgbwJSCiyfPlyZwdZO11kNqtdu3Z20UUX4Tgyb7pkAAEIQAACEIAABCAAAQhAAALlSCCqliLKsS9oU7QI4FMkWv1FbSEAAQikJSBBgvfBkTZSSE+0adPGxo8fb9IMkZP4/v3722WXXYZAJKT9RbXCSSDK94BwEqVWEIgWAe4B0eovaguBQhKI6jtAIRmQFwTiSoDnf1x7nnbnSwBNkXwJkh4CEIBASAg0btzYCRRq1Yrmrb179+42duxY27Rpk40cOfIQM1ohQUw1IBBqAq1atYqsT6FQg6VyEIgAAW0okIalNhcQIACBeBGQ6dn27dtbgwYN4tVwWgsBCDgCTZs2tTp16ljNmjUhAgEI5EAgmitnOTSQqBCAAATiQkCTIX3CHrZt22ZNmjQ5rJpa0Bk6dOhhxzkAAQhkR6Bt27bZRSQWBCBQdgT0DO3QoUPZtYsGQQACmQlIKNKxY8fMEYkBAQiUJYHmzZubPgQIQCA3ApjPyo0XsSEAAQiElsCePXtMAodPP/00tHVcsWKFPf744/b222+Hto5UDAJRJbBjxw7bvn17VKtPvSEAgTwIyJ645gC7d+/OIxeSQgACUSRw4MAB27p1q+3bty+K1afOEIBAngT07NccAN8ieYIkeewIIBSJXZfTYAhAoFwJyOzU0qVLbf/+/aFs4rx582zixIm2aNEimzx5si1evDiU9aRSEIgqgVWrVtnKlSujWn3qDQEI5EFAGyKWLVtm69evzyMXkkIAAlEkoAVRvQOwMSKKvUedIZA/gQ0bNrg5wCeffJJ/ZuQAgRgRQCgSo86mqRCAQHkT0CRIO8TCuEPk9ddfdwIRLdbI1qm0Wh588EF2tJb3JUnrqpmAxj+7RKsZOsVBICQE9Ozfu3evacc4AQIQiBcBjX89/8OsLR6vHqG1EKheAnr2aw4QxnWA6iVBaRDIjQA+RXLjRWwIQAACEMiBgCZoU6ZMsddee81N0o444oiKydqZZ56JQ9gcWBIVApkIaHwRIACB+BLgHhDfvqflEIAABCAQXwJ6/jMHiG//0/KqE0AoUnV2pIQABCAAgUoISJVf/kNkNkvaIZqoaQdbw4YN7fOf/7x16dKlktScggAEIAABCEAAAhCAAAQgAAEIQAACEIBA4QnEXiiiBTrZt//Tn/5kU6dOteXLl1vt2rWtY8eOds4559jll19u7du3z0vqKhU22fhX3ipj+vTp9sEHH9iaNWucinurVq2sb9++dv7559tZZ51lzZo1y6u8wl8m5AgBCESBQJh2iMi/yV//+ldbu3atE4iIn+6FHTp0sAsvvNBat24dBaTUEQKRI8Ausch1GRWGQMEIhGkeULBGkREEIJAVAZ7/WWEiEgQgAAEIQKCCQKyFIrK5J5v2P/zhD23Lli3WoEEDZ8pFC3cLFiywm2++2e655x674447nICkglqOP+T49Jvf/KYTiGiyoh3T9erVs7p167rP1q1b7aWXXrJnn33Wevbs6co76aSTrEYNXL7kiJroEIg1gUaNGjnBQ61apbu1S9AsR49///vfnd8Q3e90T9W9T8JfCX4bN24c636i8RAoFoGWLVtWmKcrVhnkCwEIhJOA3hvatGmDWcpwdg+1gkBRCdSpU8fatm3L+C8qZTKHQHgJNGnSxLQGwBpiePuImoWTQOlWzkLA4+mnn7Zvf/vbTkjRqVMnGzFihA0aNMjkrHjmzJn25ptv2rZt2+yqq65y2iNnnHFGlWqtndLSDJHJGGmd6NOnTx/r3r27aQKzbNkye+utt5yJmZUrVzqzMg888ICNGTOmSuWRCAIQiCcBaZnpU6ogQfPs2bNt0qRJrgqalEkgovvcMcccY6eccor7Xar6US4Eyp2A5hcECEAgngT0zNX7DAECEIgfAW247Ny5c/waToshAAFHoEWLFqYPAQIQyI1AbIUiH3/8sV177bVu93K7du2ctshFF11UQe/qq6+2u+++22688Ubbt2+f3XDDDTZ06NAqmXzRTukBAwY4Qci4ceNs5MiRFeX4H9JU+clPflKxu/qaa65xZrYkSCFAAAIQyIaA7lUy1Ve/fv2S7BJ55ZVX7OWXX3ZlSzNEWiNNmza1E0880YYPH55NE4gDAQjkQUB+fCSIlOYrAQIQiBcBjf1du3a5naLSRidAAALxIaBNnXv27HGbj2QKnAABCMSLgDYnHjhwwL0DYEovXn1Pa/MjEFv7TPIhsm7dOnfTkC+PoEDEI/3KV75iZ599ttMckRBFaaoS5J9EwpWbbroppUBEeWp39/XXX2/9+/d3LzMqT5osBAhAAALZEti4caMtXrzYCUayTVPIeFqE0YuYJmKalMl/iMxlIRApJGXygkB6AitWrHD+y9LH4AwEIFCuBLQRYcmSJe79plzbSLsgAIHUBLQpQu8A27dvTx2BoxCAQFkTWL9+vZsDSEBKgAAEsicQW6GIHABrAU+2N+X0N12QeS29ZOzcudOZ1NJO7FyDHAofddRRGZOpPqNGjXJCEdkDnDVrVsY0RIAABCDgCUgQoV1i2i1aiiAtuOOPP95p10nAK4FzNve+UtSVMiFQjgS0S0z3AAIEIBA/Anr2a/xLY5QAAQjEi4AWQiUY0bsAAQIQiB8BPftLuQ4QP+K0uFwIxNJ81rx580yaGDJr1apVKxsyZEja/jzyyCOtV69etmjRIlu9erW99957zoxW2gR5npCDRK/uJn8mBAhAAAK5EPD3j1zSFDLu6NGjnT1TCUNwqF5IsuQFgcwENP5LfQ/IXEtiQAACxSLA+C8WWfKFQLgJ+Oc/94Bw9xO1g0CxCPh7QLHyJ18IlCuBWGqKzJkzxy0ayMxLz549K+1b3VyOPfZYZ0Jr69atRTdLsXz5cqeZokq1bNmy0rpVdpIJUWV0OAcBCORLQGZ6UgU5etQ9E4FIKjocgwAEIAABCEAAAhCAAAQgAAEIQAACECg1gVhqisyfP98JRWSiqnPnzpX2gYQLXbp0ceZoZKNTGibFDHJSLLVXmew66aSTqlyUHC3K1JdMaSQHacjghDWZCv9DIPoEZDpDnxo1iifvlmru3/72N9uyZYuNHz8+5T0UoWz0ryVaEE0C/h4QzdpTawhAIB8CevZzD8iHIGkhEF0CGv/Mv6Pbf9QcAvkS8OO/mOsA+daR9BAII4FYCkU2bNjgJg3efFamjmnXrp17yZCAYceOHZmiV/n8b37zG1u2bJkrSw6Kx40bl3Ne/mZ47bXX2nXXXXdYeglbZNbm7rvvrnDEJhukOla/fn2TNszSpUsPmVQpz06dOlnz5s1t8+bNtmrVqgptFhWgG68YyRSZ0ut8sj3T9u3bm3yrSFgjJ3Cqhw/6LcFTixYtnEaONHnUNz6ofl27dnXnJej58MMP/Sn3rfQqXx/ZUlX9g/aUVX85spcATOe1w13fnpUyUdt0XvkrffC8XjDVNvWJgsoP5q/y1TaVr3rLxFqwfUqv8r1w7aOPPnL5u8z++Ue763v37u3yFT9xDNbPnxeLlStXun7wDzzl36hRI+vevbvrC9U/mF7ntWtffVinTh3HP3gd67yEZKqf/Nokp1cVJUDs06ePa5/MyK1du/aQhXflMXjwYHftysmXhIe+fkovLmqftLPWrFnj0vv2Ka3KVfmqh/LXGNVxH5RO9Vc7VPa6desOOa+yunXr5jjoGlUbgteQ+kPXuDhKuClzeMHz2YwBXR/qx0xjQMICtT95DOj60XUk/0RyhBq8RvQ73zGg/P0YWrBgwSH5i7WucTHUGNQ1FLzGxTnTGJBpP/XTn//8Z8dA/fP44487HyLiqrZV5xgQ5+A1lMsY0BhUP/igtjRs2ND1gcaIrp/gGFK8XMaArk9dx8ljQGNI+eQ7BpRe4yw4RlSW7gFqR6YxINOMYpA8Bvr27evGiNouBp6v2q/flY0B5aX+l4ZjqjGg9HoOVDYGdJ/XdaixM3fu3MPqpzGu8+meA8pffsJ0bWuMBcegys80BjR+NEb0rNezODhGxFp179ixo+Oe6jmgMSIG6gvVP9g/+u3HmH7rHpTs+0PPYJnr1PNFY1T9FOwD/xxQu/ScSB4DTZo0cXVUfyVfwypT90/1oe6nmcaA+CWXr3S6j6uvdY/TdR68xlWGfw6kGgO69pVe3xofiuPbp7Rqn+6D4qD8N27ceAhDjU31j8Z6pjGwadMm14fBa1xlaAyqnKqMAbVV/Jo2bWrKX3UM3sdzGQN6BquPgun1O98xoOeArkPd55V/ZWNAcyFdg74PdN1kGgOa66gM1VUbjJLnQpnGgM9ffbFw4cLDNu5kGgP+vNqlMRK8TytPXeO6D6pNydewzmuM6BpKNwZ0bamP/VxJ84Ugn0xjQAwHDRrk+Oj61nUaHCOFHAOp5kqqqxjrk+o5kM0Y6Nevn3vW6/6i+2Cw/bmMAZWvOiZfg7p+9JzINAZ0bWk+HRzDmqt1CzwHZA45WD9dl7pH+/eNVGNAbBRHcwBdQ/mMAT0Hgu1T+XoG6SNWyc8BnVfbdQ2mGwOaB8uKgdqvMZp8DRZyDOg5pHuFD6pTpjGgsaHnlNqne6DmIsFrPJsxEHyfyPQc0PtAMASfA6nGgK4XXSOaC+kZIgsMwWtIbdT7gOazun/onTR4XteYfOKpnHRjQM8p3UvSPQc0F/FjUIyUpw+6Xv0YUN9qjOm68EH103NA7xvpxoDucTqvuYquQT8GVI7mLf6+rL7VfTC5/ExjQNeoxojGhuoXHCOqX/A5kGkM6J0+GNTW4Fwq1XOgUGNA5eoeEJzLqP7B50CmMaC5ku5VnrHyDI4BzcV0jRZ6DOga0FxF+es6DgaVr+eUrvFUcyVdz7pGxFF1030keI0XcgyobqqD8vRBZfkxoDGicZp8DWY7BjTff//99w+pv/JKNwZUB11jusfqOks3BnSNqw7+OZC8iTjbMaC6aC6U/BzQu4DmQ7ouKhsDqqvGgNoZDOrbHj16uON6TiU/B4JjxL8vqCw//nXdywWAgu4BwfTqK81jxUhzklRjoLL3BeWp+6eeUyoz0xjQuk3yupHK9c8B/xzxY0z107Vf2RhQ+ap/tmNAjIJjNDgGdJ8Xo+AYUb9meg6ofuKUbgzo+tJ7X6YxoPtT0FKQ+Kp+/jmQbgyof9WPweeA0unZoLSE3AjEUiiiG4OCBp8GXWXBx9FFphueJgbFCO+8845bbNSDV2Vdf/31hzyAcy1T9Q4O/mB6Hdd5fZKDP+7PqS7+mI8b/N+f9+f07dP67+Cx4G9/3n/rnIL+Dx5L/h3838d3Cf/5x5/338FzPr7O+fNqQzD4c5Wd9/kEv30ePr3/Pzl/fzxV/v5YMN/k9D5/HzddHwTP+zL9tz8X/D94TL+T//dx/XfwfHIdFcefz1S/YFyfd/BYqvQ+ni8jGF/H/CdVPB83mNbH89/B9KnKr+x8MN/gb5+3Lz/5Oxg3mH8wns8jGNcfUz31MquJhh7ummiliufjB8tIbqM/F0yvB/aLL77oJmj+vB70KksTh2Dc5Don5x+sg34X47yvo8/fl6nv4Lng8eDv5Dip2pd8zKf3x/13pvYpnY/r8wge8+l9HP8djBNM54+ni6fj/pOczv8fPJ9cfnL+Op8uBOsQjOOPJ3/7OMHydczH8+dTHQvG8b/9dzCd/61z/nxyG/y54Hn/26cP1iH5XPL/qfIPps/mfHKeyemVh17EFE8vRPr2aXTO//b1z+Z/Hye5fr5sf97nmfztzyen98f9d7p0/nxy+mB8H8d/+3P6P3gsOY/k80oXjB88nyptMH7yeV+HYJzk3z6OL9N/++PJ8TOdD6YLpg2mS/6t/4PHUuXhz6droz+fnJc/7vNMTu/P+2+d9799mmA7skmfnE75+TyT0wfzDqbzv33aYHr/OzmO/z9Yho/rv4PnfHx9+/PBY8Hf/nxyen/cfyuN5ve6B2jBRM9nnQueVx7B/5PPpyrXx/fl+/99XP9/uvOK5+P4NMnf/rz/Dp4PHtPvyv4PpvO/g/H9Mf/t8wvGCf5WPB9Hv30b9VvBx/XfB48e+jd4LphXqvSZ8k8+70vyZaQ6Hywz+XzwnM8r+TsYJzm9b4MvX/+ni5PunM9D36lCct7B/338VMeC54Lnk+unc8HzPl3w259PTuvjBM/738nn/HH/rfOpfqcqI1W8YP7+vOb/WqTTorkPOufPB4/53/pOFcef9+d8Hqqf/+3TBr99Ov8djJtNvOT8k/PJdD5VGb4NOpcqfWV19Gl9nOT0yed9GfoOhnTpfRx/Xv8n/w7+n6r8VGmC+Sp9MA9/zn9nOh/MX+UH/09Om6p+vuzkb1++//bnfRn+eLA8/9vHTfV/MF025338YJ7+WDC9P5+qjT5eMJ3/7dNl+t/HC7bfH9O3/5183h9X/hKaa20zuAbo0/p4wfS+TvrO57wvI5if/+3z9d/Z8PNxk/PQ/5nqrzjJ6dMd03EFxU+V5uDZg3/9+VTl+3OKmdw+HfPng9/B34qj4I9lKsPHO5jq4F8dS1V2MA6/0xNwq+IJgOlXNNKnjeyZyy67zKZMmeImDjfeeKNdfPHFadsiNBMnTrQvfOELbmfZ1772NbvhhhvSxq/KCe0k+8Y3vmHTpk1zN7HLL7/cfvazn+Wc1ezZs+2ss85yi6FKr3YlS76VqSShumkSIACB8iKgnQ26Z+nFqJBh1qxZ9swzzxwyEdGOWe2IPfPMMwteXiHrTl4QiBMBv1ut0PeAODGkrRCIMgHdA/RyHNz1GOX2UHcIQCB7An5DVKpFs+xzISYEIBBVAroHBIUiUW0H9YZAdRIo7MpZddY8j7K8QECLh8nqaqmylWDBv2BIXauQQTu5JGTxAhH5ESmE0EVt1KKlPgQIQCAeBLxJGZlBKsS9Smq4U6dONQlFdL/0uxCAMgCgAABAAElEQVSkkTJs2DAbOXJkPMDSSghEhIDU8PVCJO0tAgQgEC8CEojI1AjmE+LV77QWAiIgSxgyhSPTSjIdRIAABOJFQFYdZLpS7wBsjopX39Pa/AjEUigiW5IKWuQL2lJNhVJxvLkt3VwymdtKlUe6Y7Jlesstt9iDDz7o6nLCCSfYnXfe6dTe06XJ9rjqTYAABOJFQIuhsiVciPEvO9EvvPBChb1iCUS04CI7saNGjbIBAwbECy6thUAECGiM6j5AgAAE4klAcwBpjRIgAIF4EdCznzlAvPqc1kIgSEDP/kKtAwTz5TcEyp1ALIUicnyjRUPdOOT4J1OQgygtCEr7Qs7JChG0A/v222+3++67z9VjxIgRdttttzlbwIXInzwgAIF4EtC9Kt8gx5PyH6IdJxIG636pFy05RRszZoxzTpdvGaSHAAQKT6AQ47/wtSJHCECgughwD6gu0pQDgXAR8GPff4erdtQGAhAoNgGNfcZ/sSmTfzkSiKVQ5KijjnKLfJKkavGvsqDFQJmjkG0+aZhol3S+QTs5br31Vvvd737npLnHH3+83XTTTdazZ898syY9BCAAgSoTkOBDvommT59u0hSR+T2/63z48OFOQ0ROHAkQgAAEIAABCEAAAhCAAAQgAAEIQAACEIgqgVgKRY477rgKTZEVK1Y4vyJ16tRJ2YcSirzxxhvOYWHz5s0LIriQz5B7773XOUEfPHiwydn7wIEDU5bPQQhAAALZEvAacNnGT463bt0651BdfpQkEJGQRBpyp5xyih1zzDGW7j6ZnA//QwACpSEgIaYXZJamBpQKAQiUkoC04DUXIEAAAvEioHGv5z/jP179Tmsh4Alo/GM+09PgGwLZE6iRfdTyidm2bVsbNGiQmzhoN/TLL7+ctnGvvvqqaaFQmiJyXNarV6+0cbM58aMf/cj+8Ic/OIFIjx497Oabb3aLjdmkJQ4EIACBygg0bdrU3aeq6lxNzlkloJUwRJp0LVu2tAsuuMA5VUcgUhl5zkEgHATatWtXEI3WcLSGWkAAArkQ0LtKp06dCmbqN5eyiQsBCJSWgPyeavw3bNiwtBWhdAhAoCQEZOZf94CaNWuWpHwKhUBUCcRSKKLO+tKXvuQEExJ4PPTQQ2n7T1od2jEtLZEzzjgjbbxsTshElnyI7Nu3z1q3bm133HGHW2zMJi1xIAABCGQiIBN/WhStqlCkQYMGzkRW7969nd+Qiy++2Pr27euEwpnK5jwEIFB6AppbaOMHAQIQiB8BCUU0B9AGBwIEIBAvAnXr1nWbIjSXJ0AAAvEjoM2RmgNoLkCAAASyJxDbETN+/PiKHdGTJ0+2H//4x253tEcn9bOvfvWr9sEHH7gbi/x9XHrppf50xff27dttwIABzqyWTGHJFn+qIAGITGZp97Vs8t9999124oknporKMQhAAAJVIiDnavlOhCQAvuSSS2zChAnWsWPHKtWDRBCAQGkI4GSxNNwpFQJhIaA5AI5Ww9Ib1AMC1UuA8V+9vCkNAmEiUIh1gDC1h7pAoLoIxNKniOBKxfS3v/2tSTiyc+dO+9WvfmUTJ050pqxki2/atGkmgYduLpK4SqiRKshu5+7du23Pnj1uMTKVLe+pU6faPffc4+IoP6m1Pfvss/bkk0+myrLimOooc1sECEAAAtkQkI+kNWvWOIGv7h/pgu5b8+fPty5duliqHWWo3qcjx3EIhJvA3LlznT3hIUOGhLui1A4CECg4AZm+fPfdd535rCOPPLLg+ZMhBCAQXgJbt251c/tu3bpZmzZtwltRagYBCBSFwJIlS2zjxo2mjdqydEOAAASyIxBboYiEE9LweOCBB+y//uu/bPny5fbxxx/b0qVLnSBE9vO1MNi1a1cnMJH/j3Qh6NgslXMzTVK840Odf//9923OnDnpsqs4LhU4hCIVOPgBAQhkQUD3tsqCnKhPmTLFabUNHz7cxo4da1K5J0AAAtEnoPGf6R4Q/VbSAghAIB0Bxn86MhyHQHkT8M9/7gHl3c+0DgLpCPh7QLrzHIcABFITiK1QxOMYNmyYTZo0yR599FGTU/VVq1Y5e/zt27e30047zTkZ9nFTfcuRUb9+/Zx/kvr161ujRo0OiybbvrLLL42UXAK7tXOhRVwIQCATgU2bNtmLL77ohLIS/M6YMcPtKJUpP16iMtHjPAQgAAEIQAACEIAABCAAAQhAAAIQgEA5EIi9UESdKOHDFVdc4T65dqrSPvXUU2mTSTPk5JNPdp+0kSo5ofQsVlYCiFMQgEBWBJYtW+YEIosWLXKaIbq3SEPkww8/NGmMSMBLgAAEIAABCEAAAhCAAAQgAAEIQAACEIBAuRNAKFLkHs5XoJFv+iI3j+whAIEQEZCgI5VfI5nrk2+jdevWOUGI4kgIMnDgQDv99NMRiISoD6kKBPIhoLGd6h6QT56khQAEokNA419zAQIEIBAvAv4dgPEfr36ntRDwBPT8l8l+AgQgkBsBhCK58SI2BCAAgdASaNy4saub1/rQxOiVV16xmTNnOvN9MpklR6zScJN2iMxm6RgBAhAoDwKtW7dGKFIeXUkrIJAzgRo1ali7du2sQYMGOaclAQQgEG0C0v6W+W/Gf7T7kdpDoKoE5I9YDtY1FyBAAALZE0Aokj0rYkIAAhAINYHmzZubfBhJw2zHjh327LPP2gcffOB2jdSqVcv2799vrVq1sjFjxtiAAQOYNIW6N6kcBHInoAURAgQgEE8CWgjp1KkTZnfj2f20OuYE6tWrZ507d2b8x/w6oPnxJdCyZUvnKxRLM/G9Bmh51QggFKkaN1JBAAIQCB0Bqc1qUWTVqlX2zDPP2MqVK10ddUxaI1osOeuss6xLly6hqzsVggAE8ifg1ea9tlj+OZIDBCAQJQKaB2hBhJ2iUeo16gqB/Al481ka+yyK5s+THCAQNQLefCbvAFHrOepbagIIRUrdA5QPAQhAoEAE5DPk3Xffdc7Tt2zZUvFSpElSr1697LzzzjOp1hIgAIHyJLBo0SInAO3Xr195NpBWQQACaQlIKCrtUD3n2fyQFhMnIFCWBKQhvnjxYrcBSjvGCRCAQLwIaFPk5s2bTe8AshBBgAAEsiPAaMmOE7EgAAEIhJ7Arl27bMGCBRYUiGjn2DHHHGNnn302DtVD34NUEAL5EZCJPK8tkl9OpIYABKJGQM/7ffv2Od9hUas79YUABPIj4Me/NkIRIACB+BGQ31C9B+heQIAABLIngBee7FkREwIQgECoCcjJonyF1K9fv6KeZ5xxhtMQQZW2Agk/IAABCEAAAhCAAAQgAAEIQAACZUEAs3ll0Y00ogQEEIqUADpFQgACECgGAe0Ok8r8ySefbI0bN7bLL7/cTjjhhGIURZ4QgAAEIAABCEAAAhCAAAQgAAEIQAACEIgkAcxnRbLbqDQEIAABcyqytWvXPgSFTGcMHTrUBg8ebPXq1TvkHP9AAALlTUAq86jNl3cf0zoIVEaA8V8ZHc5BoHwJ+Oc/94Dy7WNaBoHKCPh7QGVxOAcBCBxOAE2Rw5lwBAIQgECoCUgjZOHChfbaa69Z0HawhCBysKpJEQKRUHchlYNAUQg0atTImjRpUpS8yRQCEAg3AZnO0BwgaEIz3DWmdhCAQKEIyLGyxn+dOnUKlSX5QAACESKgZ7/eATCjFaFOo6qhIICmSCi6gUpAAAIQyI6ANEHeeuste+GFF5zwQxOg4447ziVu3bq16UOAAATiSaB79+7xbDithgAETL7DevXqBQkIQCCGBBo2bGh9+vSJYctpMgQgIALt2rVzH2hAAAK5EUAokhsvYkMAAhAoGYGtW7fayy+/bG+++WbFLpCpU6e6XSF6ETpw4IB98sknbpcYu0RK1k0UDIGSEZDQVIGdoiXrAgqGQMkISEtU9wAJR7RrnAABCMSHgDTH9+/f78a+7gEECEAgXgRYB4hXf9PawhHAfFbhWJITBCAAgaIRWLlypU2cONFmzZplNWrUcEIRvQDJoXqDBg1cuZs2bbIlS5a4l6KiVYSMIQCB0BLQfWL58uWhrR8VgwAEikdAc4KlS5faunXrilcIOUMAAqEksHv3blu8eLFt3749lPWjUhCAQHEJrF+/3s0BtEGSAAEIZE+AbUTZsyImBCAAgZIQmDdvnkkjRAsdfveXJjy9e/e2cePGWcuWLV299uzZ416Ggn5GSlJhCoUABEpCYOfOnU5brCSFUygEIFBSAtIU0YIoWiIl7QYKh0BJCGiXuMa/fycoSSUoFAIQKBkBvw6guQABAhDIngBCkexZERMCEIBAtRN4/fXXncksLXZ6k1hSjx8xYoSdcsophzhU13kfp9orSoEQgEDJCXAPKHkXUAEIlJQA94CS4qdwCJSMgB/7vAeUrAsoGAIlJeDvASWtBIVDIIIEEIpEsNOoMgQgUP4EJPiYNm2avfrqq86huiY62vkhDZELLrjAjj76aGdGq/xJ0EIIQAACEIAABCAAAQhAAAIQgAAEIAABCBSOAEKRwrEkJwhAAAIFIbBr1y576qmnbO7cuRVmMCQQqVu3rl166aXWrVu3gpRDJhCAAAQgAAEIQAACEIAABCAAAQhElwBms6Lbd9S8tAQQipSWP6VDAAIQOITAtm3b7K9//avJYbK3C65JTps2bezzn/98pbaC69SpYw0bNkSD5BCi/AOB+BCoX7++4VMoPv1NSyEQJCCNUs0B6tWrFzzMbwhAIAYE5HOwUaNGFe8OMWgyTYQABAIE9OzXHAATegEo/IRAFgQQimQBiSgQgAAEqouAnKmvWbPGOVSXMKRGjRrOofq5557rXnYqq0fbtm1NHwIEIBBPAj169Ihnw2k1BCDg5g1HHXUUJCAAgRgS0GJo3759Y9hymgwBCIhA+/bt3QcaEIBAbgRq5Bad2BCAAAQgUEwCPXv2tLPOOsvt8qhdu7YNGzbMLrzwwowCEdVJ/kb27dvnfI8Us47kDQEIhJOAfBHpQ4AABOJJQOP/wIED8Ww8rYZAjAlIS1TvAGiLxvgioOmxJsA6QKy7n8bnQQBNkTzgkRQCEIBAMQjIibr8ikgVfsSIEVmrwW7atMm2bNliXbt2NZnSIkAAAvEisGrVKrcgcuSRR8ar4bQWAhBwGyOWLl1qjRs3tnbt2kEEAhCIEYE9e/Y407vSGG/atGmMWk5TIQABEdiwYYPJDLfeAbSGQIAABLIjgFAkO07EggAEIFBwAhJ8yP6nTGQFg3yJjB49Ongoq9+7d++2zZs3W+fOnbOKTyQIQKC8COhliF2i5dWntAYC2RKQyU1tjEieU2SbnngQgEB0CUhLTOO/WbNm0W0ENYcABKpMYOfOne4eoPcAhCJVxkjCGBI4dCUuhgBoMgQgAIFSEJg/f7498sgjpl2dhQpyrMZiSKFokg8EokdA4x8Hi9HrN2oMgUIR4B5QKJLkA4FoEdCz33+iVXNqCwEIFIKAnv+sAxSCJHnEjQCaInHrcdoLAQiUnMCbb75p06ZNM2/u6rLLLrMWLVqUvF5UAAIQgAAEIAABCEAAAhCAAAQgAAEIQAAC5U4ATZFy72HaBwEIhIaAHKA9//zzNnnyZGfzU34/1q5da08++WRBHKPKdIY+BAhAIJ4EuAfEs99pNQQ8Ae4BngTfEIgXAT/2eQ+IV7/TWgh4Ahr7mND1NPiGQPYE0BTJnhUxIVAQAnpgYd6kICgjlcnevXvt0UcftQULFjjBhdRbJSRp0qSJ8x9SCNuf9evXd84VUZ2N1KVBZSFQMAJysMwLUcFwkhEEIkVAc0s5WG7YsGGk6k1lIQCB/AnUrl3b+ROpW7du/pmRAwQgEDkCDRo0cPcA1pki13VUuMQE0BQpcQdQfPwI6EH1ox/9yIYPH25f//rX4wcgRYtfeeUVO+OMM+yqq65yDsJSRIn0ITk+vPfee01+RBR0DRw4cMA6duxoX/ziF61bt24FEZS1bt3aevXqZdJAKVRYuHChnXvuuTZu3Djbt29fobIlHwhAoAgEunbtat27dy9CzmQJAQiEnYA2V/Ts2dPatWsX9qpSPwhAoMAEtCCqdwAJRgkQgED8CLRt29bNAWrVYt97/HqfFudDgBGTDz3SRp7Ahx9+aBMmTDjMdJF22jdq1MgtVp966qk2fvz4gkrepSGgRXF9Fzooz/379ztHW/kujn/00Uc2Z84cmz17tvssWbLEdu/ebWeffbZ9//vft1atWuVd/R07dtgzzzxjixcvtmuvvdZx9pnu3LnT9Y8W5jM94LU7uk+fPvbb3/7W2rRp47PI6VumrR5++GHTdbFr1y7r0KGDnXbaafa5z33O/c4ps39GXrZsmT3wwANOoOB3bqiu/fr1cxylKaIgDSI5Xf/jH/9oM2bMcMIhvdgMHTrUvvCFL7j4mTRA0qnMqy3vv/9+RT/OnTvX+TPR8d/85jd2+umnpxXK6AVryJAhzsTXfffdZ1/5ylf+2TK+IAABCEAAAhAIGwE0ksPWI9QHAtVHgPFffawpCQJhIpBuHSBMdaQuEAgjAYQiYewV6lRtBLRILXVjLTbL3EC9evUqypbQQqaO3n77bfvLX/5iv/jFL9zCdEWEPH5osVs7+YrhXPtvf/ubEy4cf/zxpt+ZhAnpmiHBwMUXX+xMsWj3odSx9ZHQpRCmnny5cjo+ZcoUk/BJgoJgUL9I+0GCk3TtUH02b97shAoS0lRFICIh0vXXX28TJ050WhYSVDRr1swJDu666y574YUX7Ic//KEde+yxwepV+luCj3feeccJExTRC0TUjqOPPtoJInTt+fDss8/aD37wAycsa968uekjzQwJavS57rrr7Pzzz3fXq0+T/L1hwwbHQponXiCm6/j++++3m2++2Qn6VKbOqS91LpOgRWVIKPPqq686gY2EiDLRQ4AABMJHQIJVvRQdeeSR4ascNYIABIpKQPMhbWbRM7p9+/ZFLYvMIQCBcBHQRrKVK1eadovrHYYAAQjEi4D8lG7bts29A6RbN4kXEVoLgewIIBTJjhOxypyAFvn//d//3QkBtBCtBe3169fbpEmT7KmnnnK/v/3tb9vjjz9u8tuQT9CC1TXXXOM++eSTLq0WvaXlkm899TDV4roWzyXE6dSpk2nB7Y033khXdM7Ht2/fbi+//LKtW7fOxo4de5hAQ0Kq22+/3QlivFAhWIgW9CVUUd9oEeCkk04Kns76t7RLJJQQswsuuMB9JBh59913nUBBgrFf/vKXdsMNN5jM02QK0sBQu1577bUKoYP6Xf0ybNgwGzly5CFZSHjyv//7v24xU+d1Lfbo0cNWrFjhBHLTpk2zG2+80Tp37mwSdqVioQylxbN169ZDfAoorthI40NCI2m/6BpRP0rYlE1Q2qOOOsq1R9oiV199dTbJiAMBCFQzAY1pLYwSIACB+BHQPEMLIoXcuBI/irQYAtEkoI1OegfQpioCBCAQPwJ+HUBzAQIEIJA9AYQi2bMiZpkTkKaI7LHqoyAtDpljknBAmiKrV6+2yZMnO1Na+aBIt6CdT57BtFrw1sNQAoN8dglIQ0PaEbJPr9/K85ZbbnGmnYLl5fNbpqWkJSINkd69ex+WlVh581KHnfznAWlRqK2KJzNQuYZ58+a5OkhAc+WVV9p3v/vdiizOPPNM5/fje9/7ns2cOdMJOi699NJKuUqoJnNgMjnmtTC0SCnNIAlDBg4cWJG//3HnnXc6rQ0JPX7+859XOEkVdwlH5KRdnO655x5nykqCqlRBvJKvL9XhxBNPdOXqelZamUT74IMPXJ+myifVsfPOO88JUiQk/NrXvlapxkqq9ByDAASKTyDVPaD4pVICBCAQFgLcA8LSE9QDAtVLwI/95PeA6q0FpUEAAqUioLHv1x5KVQfKhUAUCSAUiWKvUeeiEJB2SKogJ9PSENHCtHb0y79IMMhkkRaY9a0FcWkbaDf+gAED0mpryBSRtC5k6kk+K3yQ4OGtt95yi9aDBw+2Y445xrRYrwV2aVPIzJNUoiVE0AJ6MGzatMlpO0i7QYIR1edPf/qT2zGofJV2xIgRbpE9mwemtEOkleBDoXcgS1AgPxfyUyJn49loYPi6+G+xkZN2CX+kyVAV56JKL3VzaVGoHsmhb9++zgn8okWL7MUXX3QaLVJNTxfEWuapVCddU+Kuup188slO2yY5nQQU8qeiHV4yUyXhXDCoH2RaTH5AdN0sX77caX0E41T2WxOkZMfLe/bsyUkgovxPOeUUp3EiU2VTp051HCorl3MQgAAEIAABCEAAAhCAAAQgAAEIQAACEAgjAYQiYewV6hQqAtqt700RyCyBD1r8nj59uj344INuwVqaJBKcaFFbi9BykH3JJZc44YhP47+feOIJ579CQoegUETnpWXwq1/9yvkF0YK2/IJIS2HNmjVu4VyCFGkbaAE9aIZJC/v//d//7QQx0prQ//JDoXoqSKihfFW3bIQiLlER/0igIRNOMpEl+/dV8VMhVlIVlRBCwqtcgwQWEsqoLqNHj3bmrZLzkHBDGhbSHJIAQ9dAZUIRaWJIiKF48klzwgknuH5Kp/Eya9Ysd92or5UuVZCjc12HEozJJJfMWaUK6ut0wr1U8XM5JsYS7qjPnnvuOYQiucAjLgSqiYDGv7/nV1ORFAMBCISIAPeAEHUGVYFANRLw7wDMAaoROkVBIEQE9Pwv1jpAiJpJVSBQcAIIRQqOlAzLjYB2xvsHTHAXv3btyzm3ND6kuSHzQlo4lzBCDrsfe+wxk3moa6+91i0mB7lo4Vx5pfL7oXPSBlHaH//4x7ZlyxaTGSdpn0gbRGaU5GNCi+4SkGjBXkE+P37yk584Pxgy8yVNEpmCkgBEE2QJAOSvIgwCEdVXfjek/aA2VNUhqDhLmCD7udLEyDXIIZk0asRH5ru88Cs5HwkkJAh57733nHCqZ8+eh5mpCqaRCTaZ8pK2kAQJ6tN0QU5RpbkhLZd0jhG7dOnirhf1ncx9pQsqVyzStSNdumyPSzgjAd2HH36YbRLiQQAC1UhAmmX+eVWNxVIUBCAQAgJ+PhScq4agWlQBAhCoBgKyEqD30MreOaqhGhQBAQiUiICe/XoHCMtaT4kwUCwEciaAUCRnZCSIG4Gnn37aLeDL3NOgQYNc87Vj/9e//rUTXJxxxhn2jW98w5l/klaBFrjHjRvnnH9Ls+APf/iD/fSnP610ET2ZqSa2M2bMcAv1N910kxN4SKPCm8DSMeXttQb08NNi+L/8y7+4BXHVWf9LU8X7GNHCvxbL9dIchiBhxNKEQElMqyIUmT9/vjMlpbaMGTOmSu2SQ0JpiYiNhBLphAnS8tBHnD/++GPnyFh9rf6Q0EyCneSgY6mOJ8dbv369y0dCrXRB5cpJu+onoVu6oD7XoqjqVowgoYgmWxJoSQsmlR+YYpRLnhCAQHYEOnbsmF1EYkEAAmVHQHMEmSJlQaTsupYGQSAjAW2069atW9p3mYwZEAECEIg0AZkCl2A03XpGpBtH5SFQRAI1ipg3WUMgMgQkKEjeWaPF8nvvvdceeughJ+jQYvM555zjFtDlW0QfaQxIECFtAE1GJYCQGSiZzrrmmmvc4rH8Zmh3fa5BQhiZw1LeWhDXQrfKUN4SxMhUlwQD0iRRUBv0EPQvw8H/dVzpwyIQ0cK6BEtqo3Y1VMV0lgQ/8uGiPC666KJc8br4Yqg8JBQR43R8ZDpKHwWZIRNjae387ne/s0ceecRdH+5kFf5IsOLLryy5+l7lBk24JcdXP+saTNeO5Pi5/u99toh5ZcKZXPMlPgQgUBgCus8XSyhamBqSCwQgUEwCmgOwIFJMwuQNgXAS0Nxf49+/B4azltQKAhAoFgG/DlCs/MkXAuVKoDjbicuVFu0qWwJyci3TU7fccktFG7VQreNatNaC+B133GEyTyRNkDfffNPtmO/fv79p93xy0MRUPipk8kqL/xKMyLdEtkFlKn0q7QFNeOWDQ9/KW7v2tSsgSkFCEWlYiJOEUdKCySUovcyXSaAhE1XdEjujqhLUv/ooVCY00iTDLzKobJmPeuqpp5w/E7VBptIuvfTSnKugvCRgUPBCl3SZ+BcdXX/pgsyBSVjTo0ePjPmly6Oy4xIMalyozhs3bqwsKucgAIESEJA5Pt1X0vkdKkGVKBICEKgmAno2S4tTmq1ojVUTdIqBQEgI7Ny501kwkPa9NMcJEIBAvAjImoWsYOgdgA1S8ep7WpsfATRF8uNH6jIi4Beo9VKpjxbL5ePhggsuMDlGl4aGggQWixcvdovk2jkvDYNUQYvcEppoZ7+cpOcSVL58gqRbKJcgQZoDEogobtSCFtb10JZAQW3MVSginyoSqqiP1D9VDZoweGGH8lK9UgV/TSi+TH49/PDDTiCiuEqzfPlyk8P0XIN2c/kdXbquKgu+fskaTcE0EhLppUjXcjGChCIKarMc3BMgAIFwEdC41HOBAAEIxI+Ans0a/9KCJUAAAvEioHcVvQPofYEAAQjEj4Ce/boHpFvPiB8RWgyB7AigKZIdJ2KVOQEtjH/5y192C+xaWNb/MumUarFeDxqZ1tJifmXOLLXgr506ip/rC6rSKH+/YJ4Kv/JXPH2iFlR3z1aT90wCgeT2TZ061QmbJBiSE/qqBjHWR/XRQkI6lqqfvy4kAJGmjvpG8VWH448/3o477rgqVcNrqGgSU1nQYqdeeNIJ4SpLW6hzfrFVvKS5QoAABCAAAQhAAAIQgAAEIAABCECgdAT0fq4PAQIQyI0AQpHceBG7jAnI3EC2ZqgkNNGCeKYd+X63TmXCjTJGmrZpemB7gZGEDZWZhErOZPXq1c6XihbopSUik2ZVDepzL2SQ6Sn1p9ccCeYpTR9vLkqCMNVfcdWGU0891QYOHBiMntPvli1bOgGDVF4rC/JlIqFIKU1ieO0eXc9iR4AABCAAAQhAAAIQgAAEIAABCEAAAhCAQNQIYD4raj1GfYtGIJ2WQHKBWhDWQrYW872T8+Q4+l/5aaFbO+or0yhJlbbcj4lhq1atXDOlAZFJSyLIY8aMGRXmyMaPHx88lfPvNm3auHqoPosWLUqpci6H4i+99JJJaCKtEmmGqG+7dOlil1xySV4CEVVY/j9kEmvVqlXOiXuqRqgOYiRBzFFHHZUqijumemV7HafNpJITXjAkwZH85RAgAIFwESj2PSBcraU2EIBAMgHuAclE+B8C8SDgx34x3wPiQZJWQiCaBDT2M23YjWbLqDUEiksAoUhx+ZJ7GRLQwni/fv3cArpMKaXzF6IF5A8++MBpE2gBvbqCFvh9COvEWJoW8scizRxxWr9+va9ypd960L/99tsuTc+ePZ3PlkoTZDgpE15du3Z1Qqs33njjMFv8c+fOtUcffdSVKa0faUdIgDF48GAnECmEYOCYY45xeUoL5JVXXklZ4zlz5rg2qz+HDx+eMo4OSnNFfnCC10DayFU4MX/+fKclI5Nf4k+AAATCRUCabzL9SIAABOJHQHMrzVPy0aCNHzVaDIHyIKC5uXz/6T2VAAEIxI+ANm7qHqC5AAECEMiewGerp9mnISYEYk1Ak035kNDikxar5fQ7Vbjvvvuc75G2bdu6+KniFOOYFsS0eK5F/FzMUmWqizRe/GK7vvP1KSHtmQEDBpjMYemTTZCQaeHChc5Hy3nnnZdVHSS4uuuuu+yXv/ylvfXWW4cVc+KJJzqTVNLUeOyxx9x5CShefvllmzx5stMg2bRpkzNd1a1bNzv99NPtnHPOqVh4lL+R5557zm677TZ75JFHnAP5wwqp5ICEIp07d3Ztuf/++50GUjC6NGmmTJli69atsyFDhjjNkuB5lT99+nS79dZb7fHHH3cLIpleiLwfE10n/ncwz3S/Z8+e7a6BTp06VbQ/XVyOQwAC1U9AY1P3EwIEIBA/AtLi1DyldevW8Ws8LYZAzAloQbR79+7Mz2N+HdD8+BKQFQ7NAVKZAo8vFVoOgcwE8CmSmRExIHAYAZkwkukmLWL/8Y9/NC1cjxkzxu3S1+K6FqefeuopNzE97bTTnGPuwzIp0oFevXq5BXwtok+aNMkt4HsBhh6SuTwoP/roo4pFevnwkFaH0su3hNeCkfaGdiXqIZxLkFBJAoEXX3zRJLiQEEcL9JUFLcovXrzYLfyPHDkyY3zlJdNTv//97535KQliVGYwSNBw0kknOVNn6kvtrujdu7cTiKi9S5cude2WZstll13m+jm4A0NCCTl+f+CBB+zkk092AjDt0sg2KK8rr7zSrrnmGvvwww/tf/7nf1w5WtiUFtJDDz1kMhmm8OUvf/mwHWDi9tprr9kvfvELp0WiOiQHmXpbsGCB6ztdC2LoHccvW7bM9aXqoY8WU2QeLlV45513XB5iT4AABMJHwN/rw1czagQBCFQHgUybIqqjDpQBAQhUPwFtWGP8Vz93SoRAWAhkWkcJSz2pBwTCRqDyFciw1Zb6QCAkBLSgr4XsDRs22LPPPmt33323TZw40U1GJTzQIr+0DSZMmGD/9m//Vq21lkmnUaNGOe0BaUhIQCNBhup11VVXuYX7bAUj0n7wDsAl/Ni2bZszNfX+++/bjTfe6PLVcQmJpKmQS5DpqmOPPdYtwMtMlZgdeeSRabPYvHmz08yRw/GxY8ea/IFkE9RW7Z5SPdNNFr761a86nyHSDLnnnnucWS8JuiTwkABIQoJvfetbJgFXUCDiy/dlSDhUlUVJ9dd3vvMdu/nmm+3pp5828dA1pj6TUEf1UPnSavHaOr5s1UdlSuAjzQ/5Rmnfvv0hbZWmy9VXX+2EdIovDSLxVB9ImCMTYQoyDXbppZc6B/Y+f/+tPpeZM6U/++yz/WG+IQCBEBGQwFP3OszbhahTqAoEqomA5p2aA0hjuBDmPaup2hQDAQgUgIB8D+pdSu8AMqVLgAAE4kVAlje0bqF3gHRrHvEiQmshkB0BhCLZcSJWmRLQC6QW2RW08JxLkJmSH//4xzZ69GgneNBOfC1iS0tAO+kvvvhipzWghefkoEVplav4yWHv3r3O7Ja+tcidKkg7YPv27S69FsCCQeVdf/31JvNdLyUchGuBXVoBiq86pcszmIf/rQU2fbwgQN/6aOItx+MKYpiqjT6Pyr67JbRLRowY4TQtpJFRmVBEWhTyuaF2SCtHdrOzCWIl1qqzOKQK6jMJeVQXmdDyfSlhigQRX//6150fEc8hmIfyl0kvMejfv7/zlRI8n+1v9Y2ES3/+859t5syZTkNFbTzhhBOcoEIm21LtAFN/+v5VG9XW5GtC5997772KlyTfj6qbNEX8NaE2SDsmVZDmk9oqrRqZhCNAAALhI6B7QPL4D18tqREEIFAMAnqWa45Ulc0ZxagPeUIAAtVHwL/vpHvXqb6aUBIEIFAKAn59yb/Xl6IOlAmBKBI4QpVODJzUK69RbFGM6yzTQmeddZZbELnzzjvtwgsvjDGN7JquxSPvd0MLzlWRqksooImo8tJQ0oKzNAf0UppqEV0104RVabTrP1mgoHMS0Ch9ujxUpoQmXlU6WXtAZSgPxfMLZKqbNAFyaaO0JbK5PaRqh+qQTXj44Yftuuuus3/913+1b37zm05DIlU632bVR8yy1Xbx6ZSnZ5oqfx1TXN+X4qb2i1lljotl4kpCmn79+jkhmQQbVQ1qm+83/c7mWlJZumZUX2mVSANk4MCBh1xXyksCuHTXY7C+qRiJyfnnn+/yl9BG+RMgAIHwEZDwU/exwYMHh69y1AgCECgqAT2r9S7QvHnzSjeZFLUSZA4BCJSEgLT558+f///buw+wuYp68eNDeg+pJCGU0BMgEDqo9KKgcAFFwYt477VelS7454IoimK7alARREAUFVEREKUYpEm5ECCA1BBiQgrpvZf/+504cbPZ9vbdPd95ns3m3dNmPmfP2XPmd2Ym8MCZ4wq1yy5wowq0qwAPmPJw4+jRo2OdR7tmxo0rUEMCthSpoZ1lVltegMp8ujxqTqJyvtIK+rQdAjCFnvpneqlpaXm2Vy7fhSq30/KVvtNSorXTQQcdFLv7opsvBk+ntUWhVEmZG7scQQ/GXuHmAc/8fUmXVKUSwYbnn38+BhvoCqw5ARG2Q9Ci2PeiVD7SMgRwCgU++KxcWUqtny62uMiilYgBkVJSTlNAAQUUUEABBRRQQAEFFFBAAQUUqHaBDtWeQfOngAL1LUA3ZMcdd1x8uplB11PLndYuNU9UMX4H467wZCVPVzc2ERShWy+CISeffHJjF6+J+WmFcv/998eWKBdeeGFN5NlMKpBVAc5JJgUUUEABBRRQQAEFFMiOAPcA3gdkZ39b0pYTsKVIy1m6JgUUaKIAA6czmDkV8G2RGDyeQdUZC4TWFfyf7iZ23XXXgi0tiuWJlkYMYM6rGhJdoxVrLdLU/NFChMHXGd9kxIgRTV2NyymgQBsIcPw3JcDbBllzEwoo0MoCtArlHEBLYZMCCmRLgHsSjv/G9l6QLSVLq0D9CvDbTxfjhXqNqN9SWzIFmi/gmCLNN6yaNTimSNXsCjNSpQI8PcEg6vfcc0/sNotupxiLgxuJ/fffPxxzzDE1XZlAWSijN0RV+gU0Wwq0skAKiHgOaGVoV69AlQpwDqBChOsakwIKZEeA6/90T2OlaHb2uyVVIAlYD5AkfFegcQK2FGmcl3MroECNCjCA+fjx48MDDzwQB1AnIMKgpIy1se+++8ZxTWr96UoqQWw2W6NfULOtQAsIWBHaAoiuQoEaFuAcYIVoDe9As65AEwVSMNTjv4mALqZAjQtYD1DjO9Dst5uAQZF2o3fDCijQVgKLFy8ODz/8cHjqqadi0IBupgiI0GXWYYcdFsaMGVMXT1XOnz8/MFbKsGHDarrFS1t9L9yOAvUmQNeABEYZq8mkgALZEuAp0alTp8aHPQYOHJitwltaBTIusHz58vD222/H7oh79+6dcQ2Lr0D2BObMmROWLl0a7wFsMZ69/W+Jmy5g2+qm27mkAgrUgACVhHfccUd48sknY255ioLuJQgcnHTSSbGVSL08XU3whxui1IVODewes6iAAi0owBhAs2fPbsE1uioFFKgVAYIis2bNCgsXLqyVLJtPBRRoIQHGZeQegOCISQEFsifAg5FcA3AtYFJAgcoFbClSuZVzKqBAjQm89NJL4aGHHgozZ87cOM4GFwo77bRTOP7440O9PUmZms7X2G4yuwoo0EICNp1vIUhXo0CNCnAOsPucGt15ZluBZgikewCP/2YguqgCNSzA73+9POhZw7vBrNeggEGRGtxpZlkBBUoL0H3ME088Ef72t7+FJUuWbLxAoAVFGlC9W7dupVfiVAUUUEABBRRQQAEFFFBAAQUUUEABBRSoOwGDInW3Sy2QAgrQfHzChAlxfI3UpyYBkRNOOCEccMABPkXpV0QBBRRQQAEFFFBAAQUUUEABBRRQQIGMCjimSEZ3vMVWoJ4FhgwZEg488MDQq1evjQOrf/SjH42f2ay8nve8ZVNAAQUUUEABBRRQQAEFFFAgOwL0lGFSQIHGC9hSpPFmLqGAAjUgMGbMmDjY2BtvvBFOO+20uhs/pNAu2GabbQIvkwIKZFNgjz32yGbBLbUCCoROnTqFfffdVwkFFMigQJ8+fWIXwRksukVWQIEGgREjRsSXGAoo0DgBgyKN83JuBRSoMgEGTicVGljsqKOOCscee2xmusviCRFehSyqbLeZHQUUaAWBUufDVticq1RAgSoT4BxAi1hbxVbZjjE7CrSyQLoH8PhvZWhXr0CVCqRzgPUAVbqDzFbVCth9VtXuGjOmgALlBFatWhWee+658MILLxSclacms1QxsGjRojBjxoywZs2agh5+qIAC9S0we/bswJhKJgUUyJ4AARGuAebPn5+9wltiBTIusHLlyjB9+vSwbNmyjEtYfAWyKbBgwYJ4DcA4qiYFFKhcwJYilVs5pwIKVJHAwiVLwpOPPRYeeeSR0KNHj0CzcZqNZjktXLgwXgwNGDAgdqORZQvLrkAWBQiIcDM0dOjQLBbfMiuQaQGCItOmTQtcA/Tv3z/TFhZegawJEBTh+O/SpUvo2bNn1opveRXIvAAPRMyZMycMHjw4dOzYMfMeAihQqYBBkUqlnE8BBVpHYNy4EKZMCeH000Po1q3sNtY3PAm9xc9/Hh5t+LF/fN680LmhNQg3AnfddVfDKk4PgwYNKruOep2BVjFeBNXr3rVcCpQXoMk8zedNCiiQTQGuAbLUQjabe9lSK7C5AMc91wAe/5vb+IkCWRDg+LceIAt72jK2tIBBkZYWdX0KKFC5QEMrj/CpT4Xw1luhob+HEM4+OzQ0cSi6/NqGIMj6Sy8Nna67Luw2bFh4vWEA9QX9+oW1Dd1F9Wt490KgKJ0TFFBAAQUUUEABBRRQQAEFFFBAAQUUUKBBwDFF/BoooED7CIwfH0JDUCNMnBjCihUhXHZZCFdfXTQva+bODSsvvDB0uv76OM+ODf3mfviWW0KnhkDJwQcfHE466SS7i2iQ8Snxol8hJyhQ9wIe/3W/iy2gAiUFPAeU5HGiAnUrkI799F63BbVgCihQUIBj3+O/II0fKlBSoPgj2SUXc6ICCijQTIGttgph111DmDkzNLT1Dg0jA4Zw8cUb/n/uuZusfHVDK5KFn/tcGPirX23y+eSGPrOP/9CHwh777BP70N1kYgb/oNls586dbTqfwX1vkRVAoFNDSzvGFTApoEA2BbgGsNVsNve9pc62QLoH8PjP9vfA0mdXgGPfeoDs7n9L3nQBgyJNt3NJBRRojsDw4SFce20In/hECA8/vGFNq1eHcN55GwIj55wTP1vZMHj4tLPOCjs0jBmSm57dc88w7Je/DENHjYp96OZOy+r/GVx5q4ZgkzdEWf0GWO6sC+yyyy5ZJ7D8CmRWgKDo7rvv7jVRZr8BFjzLAr169Qp77LGH9wBZ/hJY9kwLDG+oWxnW0L041wImBRSoXMDusyq3ck4FFGhpAVqK/PjHoaH/q03XTEuRsWPD4sWLw8unnrpZQOTV/fcPI+64IwxruPjnySjTBgEsuBBykEW/EQpkU4CAqEHRbO57S60AAlwDeF3kd0GB7Alw7e89QPb2uyVWIAmkeoD0t+8KKFCZgLWJlTk5lwIKtJbAyJEhNAycHhoCHZukhpYiHQYNCnuPG7fJx9MPPTQMv+22sOWIEaGh0y1TjsCCBQsaxqx/K6xpGHjepIAC2ROY2dAd4fSG8ZZMCiiQPQG6zuMaYF7DWGsmBRTIlsCKhvEZp06dGpYsWZKtgltaBRSIAvz2cw2wdu1aRRRQoBECBkUageWsCijQSgINLT4CA6g3jA2Sm3quXJn7Z1h0zDGh/89+Fnput90mn/vHBoFFixaFGTNmGBTxC6FARgVmzZoVeJkUUCB7AgRFuAaY3zAOm0kBBbIlsLLhnonjfxljNJoUUCBzAgsbuhznHODYgpnb9Ra4mQIGRZoJ6OIKKNACAuvXhzB6dAg33xzCttsWXOGqhhYi3X7609Bt++0LTvdDhmLZwq6z/CIokGEBms7bfV6GvwAWPfMCngMy/xUQIKMC6R7Aa4CMfgEsduYFOPbtPjPzXwMBmiBgUKQJaC6igAItLNDwIx5Tw6DpYeDAgivvvPPOocs22xSc5ocKKKCAAgoooIACCiiggAIKKKCAAgoooEAlAgZFKlFyHgUUaH2Bhiaf4eyzQ3jxxYLb2uL220P43/8NYfXqgtP9cIPAelrdmBRQIJMCHP+eAzK56y20AlHA498vggLZFEi//54Dsrn/LbUC6RyghAIKNE6gU+Nmd24FFFCgFQQaBggPl10WwjXXhIbRwTbfAC1JGDj0S18KDe1CQ/jsZ0Po5OkrH6pTg0n37t3tPicfxr8VyIhA165d7Us4I/vaYiqQL0DXGVwDdO7cOX+SfyugQJ0LdOzYMR7/3AuYFFAgewL89nfr1s16gOztekvcTAFbijQT0MUVUKCZAg2Dg28SEEldaZ1+egh//nMIRx4ZGh59ZsCMEBYvDuHSS0P4wQ82fNbMTdfb4gMGDAg77LBD6NKlS70VzfIooEAFAts2jMm03XbbVTCnsyigQL0J0Jf4iBEjwlZbbVVvRbM8CihQRoCA6I477hj69OlTZk4nK6BAPQoMHjw41gMQIDUpoEDlAj5KULmVcyqgQEsLLF26Icjxwx/+K/BBAOTMM0P47ndDaKjkDwys/ulPh/DggxsCIyzzhS9s+P8557R0jmp6fQRDDIjU9C408wo0S4AnxEwKKJBNAVqK9OjRI5uFt9QKZFyAilCP/4x/CSx+pgWsB8j07rfwzRCwpUgz8FxUAQWaIZBafVx99aYBkY98JITvfW9DQITV77bbhm61Dj30X61DVq4M4fzzQxg7thkZqL9F58+fH6ZMmRLWrFlTf4WzRAooUFZgxowZYdq0aWXncwYFFKg/gXXr1oWpU6eGuXPn1l/hLJECCpQUWLFiRbwHWLJkScn5nKiAAvUpwG8/1wBrC3VFXp9FtlQKtIiAQZEWYXQlCijQKAFagzzzzIbgR1qQzz784Q0tRPr3T59ueE+BkYMP/tfnDTf/4bzzQmioBDRtEOBGaNasWQZF/EIokFEBboisEM3ozrfYmRcgKMI1wCK6JTUpoECmBFatWhWP/+XLl2eq3BZWAQU2CCxueOCUawCuBUwKKFC5gEGRyq2cUwEFWkqA8UEOOSQEus1K6YMfLBwQSdNHjQrhuutC2G+/DZ/QX+b994cwdGiaw3cFFFAg8wLrCTCbFFAgswKeAzK76y14xgU89jP+BbD4CiiggAKNFnBMkUaTuYACCrSIQOfOIXzykyEsWBDC+PEbAiKDBpVe9R57bAiMfPzjIVxxxYZB2Esv4VQFFFBAAQUUUEABBRRQQAEFFFBAAQUUUGCjgEGRjRT+RwEF2lyA1h6XXNK4zY4ZE8JTT20YaL1xS2Zibp8Sy8RutpAKFBTw+C/I4ocKZEbAc0BmdrUFVUABBRRQYKMAv/9eA2zk8D8KVCxgUKRiKmdUQIGqEaD7LdNmAp06dQrdunULW+izmY0fKJAFgS5dutiXcBZ2tGVUoIAAv/1cA3SmJa5JAQUyJdChQ4fQtWvX0JEHzkwKKJA5AX77OQdYD5C5XW+BmylgUKSZgC6ugAIKVItAv379Qq9evawQqZYdYj4UaGOBrbfeuo236OYUUKBaBKgU3W677QIPSJgUUCBbAgREt99++xgYzVbJLa0CCiAwcODA0LdvXwOjfh0UaKSAV82NBHN2BRRQoFoFuCHiZVJAgWwKEBQ1KaBANgV4OrR3797ZLLylViDjAgRD+/Tpk3EFi69AdgWsB8juvrfkzRPo0LzFXVoBBRRQoFoEFjQMWv/WW2+FNWvWVEuWzIcCCrShwMyZM8OMGTPacItuSgEFqkVg3bp1Ydq0aWHevHnVkiXzoYACbSSwYsWKeA+wdOnSNtqim1FAgWoSmD9/fjwHrF27tpqyZV4UqHoBgyJVv4vMoAIKKFCZwJIlS8Lbb79tUKQyLudSoO4E5syZE2bPnl135bJACihQXoCgCNcACxcuLD+zcyigQF0JrFq1KvBgxLJly+qqXBZGAQUqE+C3n2sArgVMCihQuYBBkcqtnFMBBRSoaoH169cHXiYFFMiugOeA7O57S66A1wF+BxTIroC//9nd95ZcAY9/vwMKNE3AoEjT3FxKAQUUUEABBRRQQAEFFFBAAQUUUEABBRRoNwHGFTMpoEDjBQyKNN7MJRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUKAGBTrVYJ7NsgIKKKBAAYEOHTqEjh07Bp8UKYDjRwpkQIBzgEkBBbIrwDWA54Hs7n9Lnm0B7wGyvf8tfbYFuP/39z/b3wFL3zQBgyJNc3MpBRRQoOoEBg4cGPr27Rs6d+5cdXkzQwoo0PoC2223XetvxC0ooEBVClAhutNOO4VOnby9q8odZKYUaEWBnj17hp133jl069atFbfiqhVQoFoFttpqqzBgwACvAap1B5mvqhXwqrlqd40ZU0ABBRon0LVr18DLpIAC2RSgUsSkgALZFOAp0V69emWz8JZagYwLEBTt3bt3xhUsvgLZFTAgmt19b8mbJ2A/C83zc2kFFFCgagSWLl0a5syZE9auXVs1eTIjCijQdgLz588P8+bNa7sNuiUFFKgagXXr1sVrgMWLF1dNnsyIAgq0jcCqVavC7Nmzw4oVK9pmg25FAQWqSoDffuoBuBYwKaBA5QIGRSq3ck4FFFCgqgW4EHrjjTfC6tWrqzqfZk4BBVpHYMqUKWHy5Mmts3LXqoACVS1ARcikSZPC22+/XdX5NHMKKNDyAsuXL4/3AAsXLmz5lbtGBRSoegGColwD+HBk1e8qM1hlAnafVWU7xOwooIACTRVIA603dXmXU0CB2hag+wyTAgpkV4BzgAOtZnf/W/LsCtB9nsd/dve/JVfAegC/Awo0TcCWIk1zcykFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRSoMQGDIjW2w8yuAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKNE3AoEjT3FxKAQUUqDqB9evXO7ha1e0VM6RA2wkwpgDnAZMCCmRTwHNANve7pVYg3QN4DeB3QYFsCvD7z8ukgAKNE3BMkcZ5ObcCCihQtQJDhw4NgwYNCl27dq3aPJoxBRRoPYFdd93VoEjr8bpmBapagPEE9thjjziuQFVn1MwpoECLC/Tu3TuMHj06dO7cucXX7QoVUKD6BYYPHx6oC+jUySre6t9b5rCaBDxiqmlvmBcFFFCgGQLcCHkz1AxAF1WgxgUMiNb4DjT7CjRDgIGWu3fv3ow1uKgCCtSqAIMse/zX6t4z3wo0X6BLly7NX4lrUCCDAnaflcGdbpEVUKA+BZYvXx4WLFhg09n63L2WSoGyAosXLw6LFi0qO58zKKBA/QnQbQ7XAMuWLau/wlkiBRQoKbBmzZp4/K9atarkfE5UQIH6FOC333qA+ty3lqp1BQyKtK6va1dAAQXaTGDWrFnh1VdfDd4QtRm5G1KgqgQmTZoUJk6cWFV5MjMKKNA2AmvXrg2vvfZamD59etts0K0ooEDVCCxdujS88sorYf78+VWTJzOigAJtJzBz5sx4DcC1gEkBBSoXsPusyq2cUwEFFKhqAZrO06e4SQEFsing8Z/N/W6pFUgCnAO4FjApoEC2BOg+z+M/W/vc0iqQK2A9QK6G/1egcgGvmiu3ck4FFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRSoYQGDIjW888y6AgoooIACCiiggAIKKKCAAgoooIACCiiggAIKVC5g91mVWzmnAgooUNUC69atCwy0aFJAgWwKcPxzHjApoEA2BTwHZHO/W2oF1q9fH+8BvAbwu6BANgUYS8R6gGzue0vdPAGDIs3zc2kFFFCgagSGDRsWBg8eHLp27Vo1eTIjCijQdgIjR44MVIyYFFAgewKMJzB69GjHFsverrfECoTevXuHvffeO3Tu3FkNBRTIoMC2224bqAvo1Mkq3gzufovcDAGPmGbguagCCihQTQLcCHkzVE17xLwo0LYCBkTb1tutKVBNAgy03L1792rKknlRQIE2EmCQZY//NsJ2MwpUoYD1AFW4U8xSTQg4pkhN7CYzqYACCpQXWLZsWZg/f36g+axJAQWyJ7Bw4cLAy6SAAtkToJUY1wBLly7NXuEtsQIZF1i9enU8/leuXJlxCYuvQDYF+O3nGsAu9LK5/y110wUMijTdziUVUECBqhKYM2dOmDhxYuDGyKSAAtkTmDJlSnjzzTezV3BLrIAC8YGIN954I8ycOVMNBRTImAAPRr3++us+GJGx/W5xFUgCs2bNe1PKkwAAQABJREFUClwD+HBkEvFdgcoEDIpU5uRcCiigQNUL0HUGL5MCCmRTwHNANve7pVYgCXgOSBK+K5AtgXTsex+Qrf1uaRVIAukckP72XQEFKhMwKFKZk3MpoIACCiiggAIKKKCAAgoooIACCiiggAIKKKBAjQsYFKnxHWj2FVBAAQUUUEABBBhTwKSAAgoooIACCiiggALZEeAewPuA7OxvS9pyAp1ablWuSQEFFFCgPQVoNtuhQwe70GrPneC2FWhHAY5/kwIKZFfA7jOyu+8tuQJeA/gdUCC7Ahz/ngOyu/8tedMFDIo03c4lFVBAgaoS6N+/f+jRo0fo1MlTe1XtGDOjQBsJDB8+3KfE2sjazShQbQIdO3YMI0aMCF26dKm2rJkfBRRoZYHu3bvH479nz56tvCVXr4AC1SgwcODA0KdPn8C1gEkBBSoXsOascivnVEABBapagIAIL5MCCmRToG/fvtksuKVWQIHYSrRfv35KKKBABgU6d+4ceDjKpIAC2RQgIGpQNJv73lI3T8B+Fprn59IKKKBA1QgsXbo0zJkzJ6xdu7Zq8mRGFFCg7QTmz58f5s2b13YbdEsKKFA1AuvWrQtz584Nixcvrpo8mREFFGgbgdWrV8d7gBUrVrTNBt2KAgpUlcCSJUviNQDXAiYFFKhcwKBI5VbOqYACClS1ABWiU6ZMCWvWrKnqfJo5BRRoHYEZM2aEadOmtc7KXasCClS1ABUhXAMQGDEpoEC2BJYvXx6PfypGTQookD0Bfvu5BvDhyOzte0vcPAGDIs3zc2kFFFCgagTWr18fqBTh3aSAAtkT4Pj3CbHs7XdLrEAS8ByQJHxXIHsCVIZ6D5C9/W6JFUAg1QOooYACjRMwKNI4L+dWQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUECBGhUwKFKjO85sK6CAAgoooIACCiiggAIKKKCAAgoooEB2BWgpYkux7O5/S950gU5NX9QlFVBAAQWqSaBDhw6hY8eOYYsttqimbJkXBRRoIwHOASYFFMiuANcAngeyu/8tebYFvAfI9v639NkWSPUA2Vaw9Ao0XsCgSOPNXEIBBRSoSoG+ffuGzp07h06dPLVX5Q4yUwq0ssBWW23lU2KtbOzqFahWASpEhg0bFrp161atWTRfCijQSgIc91tvvXXo2bNnK23B1SqgQDUL9OvXL3Tv3j0+IFnN+TRvClSbgDVn1bZHzI8CCijQRIFevXoFXiYFFMimwIABA7JZcEutgAKxhcjgwYOVUECBDAp06dIl8GCESQEFsinQp0+fwMukgAKNE7CfhcZ5ObcCCihQtQLLli0L8+bNC2vXrq3aPJoxBRRoPYGFCxeGBQsWtN4GXLMCClStAH2Jz58/PyxZsqRq82jGFFCgdQRWr14d7wFWrlzZOhtwrQooUNUCS5cujeeAdevWVXU+zZwC1SZgUKTa9oj5UUABBZooQGXolClTwpo1a5q4BhdTQIFaFpg+fXqYNm1aLRfBvCugQBMFeCCCa4C5c+c2cQ0upoACtSqwYsWKePwbFK3VPWi+FWieAL/9U6dODQZFmufo0tkTsPus7O1zS6yAAnUqQIUIARGeFjUpoED2BDj+vRnK3n63xAokAZ4Wt7Vo0vBdgewIcO3P8e81QHb2uSVVIFeA337OAdYD5Kr4fwXKC9hSpLyRcyiggAIKKKCAAlUvsMUWW1R9Hs2gAgq0noDngNazdc0KKKCAAgpUqwC//14DVOveMV/VLGBQpJr3jnlTQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUECBFhMwKNJilK5IAQUUUEABBRRoXwGfEmtff7eugAIKKKBAewj4+98e6m5TAQUUUKCWBRxTpJb3nnlXQAEFcgS23HLL0KVLl9C5c+ecT/2vAgpkRWDIkCH2JZyVnW05FcgT6NChQxg+fHjo1q1b3hT/VECBehfguN9mm21Cr1696r2olk8BBQoI9O/fP3Tv3j107NixwFQ/UkCBYgIGRYrJ+LkCCihQYwK9e/cOvEwKKJBNgYEDB2az4JZaAQUCQZGtttpKCQUUyKAAD0V5/Gdwx1tkBf4p0KdPn8DLpIACjROw+6zGeTm3AgooULUCK1euDEuWLAnr1q2r2jyaMQUUaD2BZcuWhaVLl7beBlyzAgpUrcD69evjNcCKFSuqNo9mTAEFWkdg7dq18fhfvXp162zAtSqgQFULpHoArgVMCihQuYBBkcqtnFMBBRSoaoHZs2eH1157LXhDVNW7ycwp0GoCkydPDpMmTWq19btiBRSoXgEqRV9//fUwc+bM6s2kOVNAgVYR4IEI7gEWLlzYKut3pQooUN0C/PZzDbBmzZrqzqi5U6DKBAyKVNkOMTsKKKBAUwV4MoRKEZ8QaaqgyylQ2wIc/7YUq+19aO4VaI6A54Dm6LmsArUt4D1Abe8/c69AcwS4/uccYFJAgcYJGBRpnJdzK6CAAlUtsMUWW1R1/sycAgq0noDHf+vZumYFakHAc0At7CXzqIACCiigQMsK8PvvNUDLmrq2bAgYFMnGfraUCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgpkXsCgSOa/AgIooEC9CNBtll3n1MvetBwKNF6A499zQOPdXEKBehHg+LcLzXrZm5ZDgcoF0j2Ax3/lZs6pQD0JeA9QT3vTsrSlQKe23JjbUkABBRRoPYEBAwaEHj16hM6dO7feRlyzAgpUrcA222xjhWjV7h0zpkDrCnTo0CGMGDEidO3atXU35NoVUKDqBLp37x522GGH0Lt376rLmxlSQIHWFxg0aFDo06dP6NixY+tvzC0oUEcCBkXqaGdaFAUUyLZAz549Ay+TAgpkU6Bfv37ZLLilVkCBQFBk4MCBSiigQAYFunTpEqgUNSmgQDYFCIgaFM3mvrfUzROw+6zm+bm0AgooUDUCNJtds2aNT4pXzR4xIwq0rcDatWsDL5MCCmRTgGsAu9DL5r631NkWoNss7wGy/R2w9NkWSPUA2Vaw9Ao0XsCgSOPNXEIBBRSoSoGpU6eG8ePHh5UrV1Zl/syUAgq0rsCLL74YJkyY0Lobce0KKFCVAlSIPvPMM2HSpElVmT8zpYACrSewaNGi8PTTT4dZs2a13kZcswIKVK3Am2++GesBVq9eXbV5NGMKVKOA3WdV414xTwoooEATBOg6w35EmwDnIgrUiYDHf53sSIuhQBMFOAdwLWBSQIFsCWyxxRbxHsDjP1v73dIqkAT4/e/Uyerd5OG7ApUKeNVcqZTzKaCAAgoooIACCiiggAIKKKCAAgoooIACCiigQE0LGEps2H30v01z89deey3Mnz8/Rlj79u0bdt999zB69OhW2cEvvfRS+Pvf/x7mzJkT+/5lUKSdd9457L///k2O8PJkCE+JmBRQIJsC3bp1C7169WryOSSbapZagfoR6NGjh+MJ1M/utCQKNEqAp0S5BujevXujlnNmBRSofQGeEKc+gQHXTQookD2BVA9gq/Hs7XtL3DyBzAdF/vGPf4Srr746PPHEE4H++BcsWBCbnffr1y/ssMMO4aCDDgrnnHNOGDRoUPOk/7n00qVLw/e+973w4IMPhsmTJ28MivTp0ydss802YcyYMXF7u+yyS6O3N3fu3MD6OSEy2JpJAQWyJfDb3/423H///eHKK68MI0eOzFbhLa0CGRegD+FvfetbYcWKFeFHP/qRFaMZ/z5Y/OwJvPXWW+HSSy8N++23X7j44ouzB2CJFciwAGMKfvvb3w5nnXVWOOWUUzIsYdEVyKbALbfcEh5//PF4Hth+++2ziWCpFWiCQKaDIjNmzAif+tSnwrPPPhuDCQQ+3vnOd8aWI1xY0Hrk5ZdfDm+88Ua45pprwpZbbtkE4n8tsm7duvCZz3wmjBs3LixZsiR07tw53rgQxGBwVFqPTJw4Mf7/pptuCiNGjPjXwmX+RxCEda5atSoGRcrM7mQFFKhDAc5bt99+ewys7rbbbrYcq8N9bJEUKCZAUOSee+6J1zMrV640KFIMys8VqFMBWrv//ve/j/cC3BfYerxOd7TFUiBPgOOdBz3vvPPOcMABB+RN9U8FFKh3Ac4BBES4BrjkkkuCQZF63+OWryUFMjumCCeOz3/+8+Hpp5+Onqeddlq4++67A8GIm2++OTz00EPxooKutR555JF4cmkuPCconuJmnXvuuWfc3i9/+ctwww03hAceeCB86EMfCl27dg2vvPJK+PjHP97ozXnz02gyF1CgrgRoOs/LQRbrardaGAUqFuBhC7vOqJjLGRWoKwF++zn+HWi1rnarhVGgIoHUjbZd51TE5UwK1J1AugewHqDudq0FamWBzAZF6L7q3nvvjZWHBx98cPjGN74Ru5uhtcjgwYPDqFGjwq9+9av4f4IYjz32WGzh0dT98eqrr8YnuFme8UpuvPHGOH4I2+NFq5Af/vCHYd99940tSGihQrDEpIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAi0jkNmgyLXXXhublQ8YMCDQSmTgwIGbiTKuCK076JJq1qxZG4Mam81YwQe0QFm+fHmc84wzzgg77rjjZkvxZMcFF1ywsfurn/zkJ5vN4wcKKKCAAgoooIACCiiggAIKKKCAAgoooIACCijQNIFMBkUITjz88MOBIAStQo477riieieeeGLo0aNHWLNmTZg0aVKYPn160XlLTXj00Udjt1l0cUVQpFh617veFQjUkGbPnh1eeOGFYrP6uQIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCjRCIJNBEQYhokss+tsbMmRI6N+/f1EyxvhgwDIGSSdIwXgfjU0MoL5o0aLAOCYM1l6olUjuOg866KD4J3kkryYFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRovkAmgyIENmixwWBEO++8c0lF5tt9991jUGTx4sVNainy5ptvhtWrV8d1sK5yaY899oizEBSZOHFiudkLTicAUygV+7zQvH6mgAK1JZAGVmOQVc5dJgUUyI4AAyyn497B1rOz3y2pAkkgDbCeBlxOn/uugAL1LcBvP8c/9/kOtF7f+9rSKVBIgHNAugdI1wKF5vMzBRTYXKDT5h/V/yeTJ0+OJw0uGmgpUi4NHTo0XmQsXbo0thYpN3/+9GnTpsWgCJ8PHz48f/Jmfw8bNix+RuuUKVOmbDa92AdcCKUTIt2D0U0YwZj8RGUJ3YalAAnvqTKVedluOqnmrpPP+Dstx7zFpqfPmYeU8pWWz53O/4ttn2Vzp7Me8peb0rpYB/9Pr1SGNG+x6eWWZ3pu/vLXnzu9XP7IC/lP2+Tv9P/m5C/5pvWldaa/0/RS+Uvz5OePdZCSAevO/Y4wjc/SdP7OnZ7ywvpTKja9kvyxvvTKXWfKP9Ny1882+axY/spNZ9m0bt7TtlNZik1Pn6f50jrS8rnT+X+l+WM9lK9QmjFjRhyT6IknnggLFy7ceBynedlGse2z3lLTc/OX1sEypNz8F8of09P6mZ/8p8/S8ml6WndTpufmp9DyTOeV75fmzZ2ePsvPX/q70HeM3xSWI+VOT+tK+Ss1PeUvLcO86f8pf/ydXrnrzJ2eu/20jtx92JjpxbbPeknFpqfPN8zVsr8DPDSQW/a0rWLf4ZSHYtPLLc/0XD/+TsskgzSdfOGbm9K8aR6mp8/S8izX1Pyx7MqVK2NXn6zv3nvvDT179ozbyF0/85XKX+70YvlL6yv0HWqrYyD55uaRfKX883mh/CV/5m3M9LSd3PXzWUrFpqfP03z5y+dO5/+V5o/1tPQxkPJY6juYmz/yWyz/5C/to7TeNG9aB9PTZ8yT/l9q+6y33HTmSetL60x/My29iuUvd3qh5YvlP22jJY+BWJB//pPykpu/tE0+Y/rUqVMDLdznzp0bzwG5y6d5U/75ux6PAcqXPDBJbrkWxb5DaZ5i01lXrl/++nOnk4di37G0Do+BTb+DaV/lf8fTfik2PX2e5kvL83ljvuMszzJp//B37vJpO7nr57OUik1Pn6f58pfPnc7/i22f5XOns570O8Dnzz//fOjdu3fs7vu+++4r+Fuf8sA2WCa9WFduKjY9d/vMn7987nTWWQvHAHlO5U//5++U//RZKi+fp32Ufwynedr6d4Dt5qaUf/Ke+x1mHj5L+efvxkxPFrnr57OUik1Pn6f58pfPnc7/m5o/1p+WZxv8n1ehVOw7nuYtNj2tP82XtsH2SLnT+awtjoG03Xnz5sVu/+m2P3X5Xyh/yT/lj3ly81/J9LSPCh0D5Cf58f/c71jaVtpeqekpf8yTUu7yhaan+VIZmD93+0zns5R//m7M9Pzt8zevlIpNT5+n+XLzl9bBZyT+bkr+0jZYDw/ZpzrltE3fiwtkMihCZSGJL1vfvn2L6zRM4UvFgOt8yQgwpMHSSy6UN3HJkiXxYGMdrKtcSvMwP61TKknks1evXnFW/v+LX/wi3HDDDQUXpdzdu3ff5ADmM5Zjm5wY8lNbTWe7XODlp7T9YtPJO/NUMp3yUc7clLt8oemtvX3ywgUUqdD2y+WvtaeTr5S/Yt+R5kwn/+lVbP1pHxTyaUz+mLfUd6zc9lt7ern8lZqOUZ8+fcI3vvGNzY7j9B0h/+nFulJK0/m7kHHyZ3opv2LTy62f5dJ3qND2yy3f2tNz81fsO5Dy35Tp5D+9CpWf7ad90JT15+af/5fah8XWX277LTW9XP6KTc/9DhQqX5pO+dKLdaWUpvN3oX2Qysf0QusvN73c+ltievoOfvrTny56DihWvnLbZ7m0/mLfkeZOT4aF/Nl+uemVbp91ldqHxcqXtt/a08vlr6nT0z5O33/ec1OazmeF9kEqP9NL+RWbXm791T6dclX6HSv2HSm3fDIu5I9PehWbTjCUFvFnnXXWZueAlsh/yl+x8rXU9GLfobT+pk5P3zGPgcLHePLBt9B3jM/LfYebM72S7afvQHvkj/Kn7VfbMcDT4XQJftddd4U//OEPZLXgeToZlzsGik1P5S+2/nLT0/aLLZ87vdA+bu/p5Dt9xwvlL3d6se9IWr7QdMqXXsXWn4yLTS+1/ubmj+XT9gvlvyWns66mXGuk/BVbPn2HiuU/dzrz8MpN5dZfbnpaf7n8Mb3QPs5dPnc6D0VQD/ClL30pfocauzzzk4qtf8PUDf+m71ju9gtNL2acli80ne2nV6H1p2m8F5pOPkqtvyWmp31cKP+sv6Wms67GHgNse+zYseG0005jcVMFApkMiixbtizScCBV0sUEJxi+8Ay2zpOYjU0EUjhgSayrXErzsM2U13LLMJ0B4WnNQiLowUmRdeQnyl0sMY0DqVhq7elst1yTv+ZOTyfJYmUsN721ts++wrfc9ltzOnkot/7W/g6UW39z81fuO1Zu+609vVz+KpnOMVzsOCb/5VI5Y4+B1j1PlvNv7new3Heouetv7vLl8tfc6fV8DKSylToH4FfqO1YNvwOl8lcu/0wv9x1knlLnsXLLt/b0cvlr7vT0PWE9xVK5fVDKj3WWm15s/V4LbdgjxXzS/io3vdw5oLW/w81dfyXfoXLfsVLTPQb8HWjud7S1l2/KMbBq1ap4ikjf7+YcA2kd6ZxT6L3U+puS/7SNWvkdKHcebu53pNz6y01v7vbbe/nmfIfSd6nUd7SS8qX1FHsvtf7m5L8pxwCV57xy6wDKfUdKTa/kfqDU8pS/EuPc/OY7l1t/uenN3X57L9/Y7xD7zNR4gUwGRXIPvEq+OGkeDgpejU2520vBkVLryN1e7rKllmHarrvuGpvLc3JgvJRSyzalHOW273QFFFBAAQUUUEABBRRQQAEFFFBAAQUUUECBthUoN2522+am+reWyaAITctJBB/KtfxgnhUrVsRgCJHgbt26NXqv0mojBSjKbY+Vs72UUl7T36Xe6Qrs2GOPLTWL0xRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUyKxA8X6S6pgkjdlB87L58+eXLemcOXNiUITWF3RR1dhEN1a03qB1xuzZs8sunuZhfpY1KaCAAgoooIACCiiggAIKKKCAAgoooIACCiigQPMFMhkU2WmnnWIrEYIi06dPL6lIS5Fp06bFgEbv3r3DkCFDSs5faOK2224bu7MiyDFlypRCs2zyGdsjEUjZcccdN5nmHwoooIACCiiggAIKKKCAAgoooIACCiiggAIKKNA0gUwGRUaOHBmDIqtXrw6vvfZaSTmCIhMmTIjdX9FqY9iwYSXnLzSRwAYDuhMUeemllwrNsslnzz33XPybLrfsD24TGv9QQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUKDJApkMiuy///6BsToIeNBVVanWGwsXLgzPPvtsbLUxePDgsPvuuzcae8SIEWHgwIExKLJs2bLwzDPPlFzH+PHj43QCKe985ztLzutEBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUqEwgk0ERuqU64YQTAi1FZs2aFf74xz8W1br55psD3WwRoCAgksYjKbpAkQnHHXdcDKww+aabbioyVwi//e1vw4IFC+J0Wphsv/32Red1ggIKKKCAAgoooIACCiiggAIKKKCAAgoooIACClQukMmgCDwf+9jHQteuXWMAgkDE888/v5kaLTZ+9KMfxfFAGEvkQx/60GbzrFmzJtx3333hz3/+c7j//vsDg7IXSmeeeWbo379/bJ1y9913x3nz56PFyg9+8IOwatWqON+5556bP4t/K6CAAgoooIACCiiggAIKKKCAAgoooIACCiigQBMFtmC5hm6k1jdx+Zpe7Mtf/nL4/ve/H4Me++yzT/j3f//3QFdX69atC6+//nr48Y9/HCZPnhw6deoUzj///HDBBRdsVl6616JFx/Lly0OvXr3Cr371q3DkkUduNh8f3HjjjeHSSy+NLU8Isnzyk58Me+yxR1z/W2+9FWiVQtdadLF1xhlnhB/+8IcF11PowzfeeCO8+uqrYd68eTH/dA+23Xbbhb333juuv9AyfqaAAtUrwLll+vTpYcaMGfHFOYZTNd34nXTSSY3OOOcYxjSiy0CCuT169AjDhw+P54ju3bs3en0uoIACrScwf/788Oabb8Zjn9ajK1eujC1W+/btG7bddtswatSoeO3SmBxMnDgxXiewblrA9u7dO7ZG5TqE1rAmBRSoDgGO+WnTpsXW7ByvS5Ysib/b3I9w3DK+IecA/l9pWrp0aXjhhRcC1wKsj2Oeh7VYD+cUkwIKVL8AD1A+8sgjsd6B3HJN8IEPfKCijNM7xosvvhjefvvt+ABmt27dwtChQ8Nee+0V11PRSpxJAQVaVYB7dB7Mpvt+fvNLJeoFqPN7xzveEev9Ss3LNO4DGE957ty5sU6BZemVhnNAuW2VW7fTFahlgdJHWi2XrIK8n3feeWHRokXhlltuCQ8//HBsLbL11lvHoALBELrXItDxqU99Knzuc58ruEYGT+eChFYnnFhKnVD+4z/+I7ZMGTt2bCCIQYCEwAU3JlOnTg2LFy+OXWx98IMfDF/5ylcKbi//wxUrVoRrr702/OUvf4knuVThSZ4I8DB+Cvl3wPZ8Of9WoDoFJkyYEH7zm9/EmxaCIikwQlCECyWO6cYERbhgIuB6xx13xArRmTNnxpshKlNS4PSjH/1oOPDAA6sTxFwpkBEBAhUvv/xy7NLzlVdeCf/4xz9ixSg3Lxz/XGdQickNDA9ynHbaafF8UI6HSlXOAbRm5WaI6wS2teWWW4YddtghHHDAAYFzAJWjJgUUaF+Bhx56KPz6178OkyZNitcBHP/cq3BP0rlz53jc8kDDyJEj47XA+973vrIZfuqpp8LPf/7z8PTTT8dxFAm6cD7hIQvW8973vje2hudhCZMCClSvAPf81113XXxQgut77u8rCYr8/ve/j11083AUAVfqDzjet9lmm/iAJg+GHn300dVbcHOmQEYE6DGGe/ZvfOMb8Rjl3p9jvVDiWp7rAeoWuacvlngogoez6dmGwAiBUR4Cp76Q+wDqFqinZKgAtkX9pkmBLAlkOijSp0+f8MUvfjGeCDj50EqDiggCG7TkOOSQQ+INx1FHHVX0KUpOHNxcUGHBDQuvUumzn/1sPOHcfvvt4bHHHguTG4IvnOwGDRoUDj744HDiiSeG448/PlZ8lFpPmnbhhReGO++8MwZUOnToEEaPHh146ptyULnKxQ/vP/nJT2KQJC3nuwIKVJ8A5xMqL773ve/Fcw5/c8HDOYmgK3/z3pj01a9+NVaI8oQYFSq77LJLHBuJwCwVsLQwo/vAq666KrzrXe9qzKqdVwEFWlCA1iB/+tOf4o0Q1wVcT6QnuRnPjJuav//97+H//u//4tOeTzzxRLjkkkvCe97znqK54PqEc8Btt90WCI5w/qAFKRWiXCfwNBrnAYIlX/rSl+K0oitzggIKtLoAT4f+7ne/C7QWTQ8v0JqLY5bgCL/d6fqecwDBk3POOadovh544IFw5ZVXxlYitETn4S/uFXgQi2OfBy+ee+65GCzlATADI0UpnaBAuwpQoXnXXXfFykwe2iRVck9A19z0fkEwhPsIerignoMHMjmfUEnKOYAHMisJsrYrghtXIAMC/N7z+z9gwIAYtCB4QRAjP3E8Mw/Hc7HEvQUPYhMYpUcZzhm0DKG+kGsAjn3eqTOkLoDrA5MCWRPIdFCEnc1JhqctjzjiiBjc4MRBdJSTEZURAwcOLPmd4KKEGw5OSgQlaJ1RKlEpeeyxx4b99tsvNl3jSQ2WpbUIedlqq63iekqtI02jooNgDpWmVHQSUd5pp51i/rlxomKVChZunmgtcu+996ZFfVdAgSoV4JzAeYint7gw2XfffeMTXYx9xJMdjUlUhBIQZX3cBF122WXx3MM5js9uuummcMMNN8SboosuuiieT3hy1KSAAm0vwA0PgQuOTR7G4NqEGxce4ODagd96xi3jmP3FL34RA5pf+9rXNlZy5ueY9dClJ61hOa8cdthhsRtQzgWcA6gU5YlTnh6jcpVrhm9/+9tlr3vyt+PfCijQcgI89cm4h7QG23XXXWOQgnsS7jEIlnLc8mDVz372s3hN8J3vfCeMGTMmHHrooZtlgspOniAlmEqQ9eKLLw60RifwwfmErnS++c1vxvuEq6++Oj51TkvUjh07brYuP1BAgfYTIKBBa6/JDQ9T8lvOMc3verlETxJ0x80DEjxccfnll8c6D45xnkj/wx/+EMdPZf2cH2gxyjWCSQEF2leA32jqIenZhpYcxR685limDrFQoo6RewbuBbh+4AFsjnPqDbkPoCtNfvtvvfXW2F0XD0dxbdGYrjkLbdfPFKg1gcwHRdhh3GgQjODV2MSJiEqLxiYCLryamngyjBMYlSjc3Pz0pz+NzV/T+ngSjBshusl48skn45PgVIB+tKGLDJMCClSnABcotBR75zvfGZvDUhFKhSiBV54Oa0ziYurrX/96vIhKN0L53W7xVBhdc9DajCfGqBClgsSkgAJtL8DxTxCUrnN4UIOnuPK75KTvfwKm/O7TjQYVIzw9yg0O/YPnJrrK42aHGymuU3hS7KCDDsqdJbaWJRDD+YVue+6+++5w1llnbTKPfyigQNsJ0JUV1wEEQooFJ+jigvPF9ddfHys1CG4WCorw4BT3AFSefvrTnw7nnnvuJpUddLfBdQYPRTBWAfcNtBil9bpJAQWqR4DgxX333RcfbDr11FPjw0yVBEW4rudBSeo6uAbI/31PXYlTR8D9APcF/N+kgALtL8A1AL/HTf1NpmUI5wDqC7lPoIcc6hhyEw9W0JsEQwnwIDXHf7FhA3KX8/8K1JNAh3oqTJbKwtMiXOQQAeapL5rW5yduqLgBojULF0PXXHNN/iz+rYACVSZAazO6t+HpEJ78oOKjWMVIqazTsoSLHJalxUl+QCQte8UVV8TzA+eScePGxaa1aZrvCijQdgI0aT/llFNi//48pZUfEEk54eaI330efiBR6ckDELmJJ8JoHUrXOPz+0zVnfkCE+Xk4gwpR5uGaggFcqRgxKaBA+wgQ3CToWe53n+AJD3MxH13q5Scenvrb3/4WnwbneoIW44We/mQcgRNOOCEuTjfCdKlXqJuO/PX7twIKtI0AYwHR6puHnc4+++zYK0QlW2YcMcYm4/qecQP+8z//s+Bin//85+M9B+cSHsLiISmTAgpUhwDHb1MTD1DTFSd1gjw4kR8QYb1cc/AQJS3HaDny17/+NXbX29RtupwCtShgUKQW91pDnh999NGNzegYHK1Y4gSYugCjgvSFF14oNqufK6BAFQgQBGmJxJPfXEjxFCjd8BRLnB8OP/zwWAnCxRCBEZMCCrSPAMEJXuUSfQgzwCqVl3Srl//EKDc3PFXKuugKg6b3xdLQoUNjd12si5YndLljUkCB6hbYcsstY9e7XDPw252fGCuAPsKpSCWAUqx1OueIAw44IHAeoFL0nnvu2Xh/kb9O/1ZAgbYV4Elvur9h3C+u5Y888sh4nFZSUcr1POMIcQ4oVVdAEJYuuXgQg+uAP/7xj21bSLemgAKtIkBrUX7Xudd/97vfXXQbPJD5jne8I54r6EqPQKxJgSwJlL/zzpJGjZSVCyO6u+CCiKe+Ro4cWTLnBx54YHzanAsdnhozKaBA/QvwxCeJm5xCT4jnChA85fxAxSoDvZsUUKC6Bfj955hNQdT8ChK6zKIFCTdDw4YNK9lHOBUijHPG+ni6/K233qruwps7BRSIwUuCIRy3hcYA4AlxWooxnW756I6vWGJ5zhOcLx5//PHY93ixef1cAQXaRoBj98EHH4xjiNH1zRlnnBG71OXzStLzzz8fxyjj+oBu8UqlQw45ZGOwhRajJgUUqA4BWn4T2Fy6dGkMcnKvXsk5gIejqAvgwQe64i7V3T/3ErQiYTu0PGfgdZMCWRJwTJEa3NsMssYAqlzk0K9wuUTXWgy4zAn09ddfLze70xVQoMYFqAzhwobUpUuXOD5JqSLRvRbnEypSOb+YFFCgugV4MOLNN9+MmeQJbwIbuYmbGuahIpRWJVR4Fks0q6fChXPG8uXLY7d7xeb1cwUUaF8Brv9pHXbjjTfGoAe/3R/+8Ic3yRSVJqnrPK79d9ppp6Ld8bEg5wfOE1SMcF6hMoXu/EwKKNB+Aozzc+WVV8bf8Q984AOxNUelueEagFYiHNMEOxlAvVSiPiG1UrW1aCkppynQNgIcu/yW03qTsQMZJ5CurgYPHhzGjBkT9txzz9iFZv54gil3r7zyysZjmoeoGV+0WGJb1BdyvcDDFjNmzCg2q58rUJcCBkVqcLfSrI0bFm6EGHC1XKLChJMdJzorPMtpOV2B2hcgKMLxThoyZEjZAjE2AecTlqHvUd7TzVHZhZ1BAQXaVIDgJX3+UmFCSzC6vskfhJEWH2lMEm6GuAYolpjG8rxz3FPhSnCk1JPlxdbl5woo0HICjPPDeCEcm/xGU1lBhSVdYrz44ouxtfixxx672VgBPFHKccxydLOVHzTNzyHjlzH+ICkFRktVoOQv798KKNCyAvzOjx07NnabdXhDF7f5gc9yW6NSkwpVzhuV3AdQV5Cu+xcvXhwfvuSBCZMCCrSPAL/fXOczEDrHMX/zIvFOl1eME3T66afH4Eh+LrkPYD6CogRSSiXm4+EI7v95QIpu+0wKZEnAoEgN7m1uijhhcYKs5KYl9SPM/FzomBRQoL4FOM453kmVnCOYJ11wcW6hQqXQgKz1rWbpFKgNAbq4Y9BVxgti8FT6GM+vvCC4yU0OgZFKnvhmeYIgPHBBpSgVMgZFauP7YC7rV4Ducgl6pAAnlZYc9xybjBN08sknh4997GPxs1wFjl+eEud3nd/ytHzuPLn/p9KEcwDzs40FCxbkTvb/CijQxgKMCXbTTTfFsX7OPPPMsP322zcqB1zHN6augO51uGZIiWuIchWpaV7fFVCgZQXoMovfblqEELDk//w287s+derU8NxzzwUekv7a174Wxwu+4oorAg845iYeqiCxHA8+lEu5dQG0SCWoWqwVSrl1OV2BWhMwKFJre6whv5yk0lPg+RUhhYqTOw8XSSYFFKhvASo1U1Ak9/gvVurcyk9uojjHGBQppuXnCrSfADdBX/7yl+NT4FRiUFnCTVN+4saJlCpR86fn/818dLVHUITjnxsykwIKtK8Av8NHHHFEPI75TeehKM4BvHO8UmHBsZ5f4cHvOJUaJIIoHN/lEoEREhWj6fxRbhmnK6BAywvQ9dXll18ej2+OfwZYb2zid5zzAKmxFZuca7iPMCmgQNsL8BADDzsxBggveoVJxzD1f3Rx+fDDD4drrrkmjgP4pz/9KQZE/ud//meTB6RSnR+/6ZXUBaRtUGIerOCV+1nbS7hFBdpOwKBI21m32JZyn+RIFZ+lVp47TyU3RqXW5TQFFKh+gcaeI1KQlZKxbO7y1V9ac6hANgSopLjooosCLUW4wTnuuOPCRz7ykdg0Pl+gKcdwulbwHJCv6d8KtI/AtttuG7vQ4Zjk+CTQQYXpvffeG2699dbw7LPPBp4o//73v7/JU93NPYa9V2if/e1WFUDgS1/6UqAbXJ78vvjii5uEknsNkH7bG7Oi3OUbs5zzKqBA8wR44IFgaKHEb/OOO+4YXwRL/vu//zuOH3j33XfHVqUMlp5S7u94JeeA3Hk8/pOi71kRKP/oUFYkaqicRG3TE13pSbBS2c+dp1y/wqXW4zQFFKgNAVp+pAua3OO/WO55oiwlLqJ8MiRp+K5A9Qhw8zNu3Lj4hPjBBx8cK04YL6BQosssbnB4UpTWH+USgdE0H8d/ue52yq3P6Qoo0HwBfssZAJlBknnfZ599YjCULjMuuOCC2I3W/fffH/+fu7XUHRafcVynJ8Zz58n/f2odxnnDe4V8Hf9WoG0EGC+ICk5aeH3mM5+JXWQ2Zcu5dQW51/iVrIv7B88BlUg5jwLtJ3DooYfGsYZo0UHrkeeff36TzKRjmN/0xtYFcP4hOGNSICsCBkVqcE/TnJ4bHi5a5syZU7YEaR7mz29iX3ZhZ1BAgZoT4DhPQZG5c+eWzT/zMD8XTlSGpgFXyy7oDAoo0CYCjBtA5SfH6ciRI8MPfvCDOChisY2n33oqOulqp1yiFUrqLoMbKYMi5cScrkD7CVDhed5558VACZUXnBsYkD0lPiMwyvmCfsVTwCNNz3+nUoWKU+YnQFos2Jq/nH8roEDLCcyfPz9cddVVMZDJE98f//jHC66c459jlZT7NHjuzFzH8zvOfJXcB7Bt7gFILJOuIXLX6f8VUKB6BAhaHH300RvHDnrrrbfimKAph+l3nN90ju9SiWOf8wTnE/5Pa/RKutwqtU6nKVBLAnafVUt76595pbkcJ0IuWhhsqVyi/2FOcMxPkzuTAgrUt8CIESM2tiZ7++23yxaW8wjnBy6G+vfvX3Z+Z1BAgbYT+OxnPxsrPbmxoTuNm2++ebMBFfNzs9NOO8UbJSpHFy9eHCtFiwU6WC/nCc4B/J9BHXPHGcpft38roED7C3C87rvvvvHpUMYAeeSRR8IBBxwQM0ZAZMiQIfHan0HTywVGc+ehMtUBltt//5qD7Am89NJLYd68ebHgs2bNCqeffvrGMUSTBtfpVHAyXgDnAO7xzzjjjNj///YNg7GfffbZcQwCrhVSq3F+32ktlnqZSOvKfWc9/P6TqEzl2sGkgALVLcDYgry4Bli4cGF8uIHffxL3AdT/cezT7Wa5xDmAcwr3CgMHDiw3u9MVqCsBgyI1uDup8OQpES6M/v73v5ctAc3pOClyMbTzzjuXnd8ZFFCgtgWGDRu28YaGJ0AnTZpUsgn+c889Fy+EuAnaYYcdarvw5l6BOhL4whe+EO66665Y4UHA8rbbbosVHuWKyLyDBg2KARFai3Kzs9122xVcjGb1r7zySrym4GaK5UwKKFD9AlReci9ARUZqFU6uecKTSlGu/Zn+2muvhQMPPHDjdUF+yTg/sDyVolSkFAug5i/n3woo0HICKSjB8fz666/HV7m108LzoYceig8+EFBJgyv369cvthZLy7/wwgth7733Tn9u9s70tP1ddtlls+l+oIAC1SfAMZu6xuKB6dzfbo5jziUkHowg0FrsgQeuFSZMmBDrCumRhusHkwJZErD7rBrc2wRFiOByoiMy/Mwzz5QsxdNPP70xKPKud72r5LxOVECB+hA46KCDYkF4QuTRRx8tWaiHH344VpzwVBkVJyYFFGh/gSuvvDL8+te/jl1p0JXFb3/728DvfyWJAOchhxwSnxCjwnPixIlFF+M6gq53qDylQpTBnU0KKFD9AjNnzoyVoVRoDBgwYJMMEwTlWOa45vhO3eNtMtM//+D8MH369Hi+oNue3IqVQvP7mQIKtLwAxx2BTgIapV605kqVnRzfzMuDEDwxntsahDGICJAyz1//+teSGX7sscfiuYT1Hn744SXndaICCrS/AAERfrvTmEGcA3JbeXMuoC6A+WhFUqq+MNUVsAyBE8YwMymQJQGDIjW6t4866qj4xBcXLzfddFPRUjBYG0+OcMPEDZItRYpSOUGBuhI4+eST400TLUV+97vfFS0brUj+9re/xZsmbsaOOOKIovM6QQEFWl+AG5ixY8eG66+/Pj71SWXnLbfcEnbbbbeKN84TY+95z3vizdDkyZMDFR7FEjdVdL3DzdCee+5pN5vFoPxcgVYW4Fq90sQT4Tz0xEDqLDd69OhNFuV6f4899ojH9Z///OcwY8aMTaanP6hQ4RqALnaoPD3uuOMcYDXh+K5AGwocfPDB4cknnwwvvvhiydett94aAyAc97TuZv6XX3453H777Zvc53Ms0/qT4/pXv/rVxpYg+UXiCXIenqJilCDK8ccfnz+LfyugQJUJMAYI5wKOWcYC3HXXXTf77T711FNjsJOWoPfcc0/REtCrzFNPPRWvF3iYgoCqSYEsCRgUqdG9/eEPf3hj3/90rVHoCRAucq6++urYmoRKlnPPPbdGS2u2Fci2AE+PEQDlBig9HVZOhJsaxhAiKEI/xTfeeONmizDWwBe/+MX4Oes+6aSTHGR9MyU/UKDtBBgQmWOVoAjHJzcnP/nJTxp9g0KAgwAnzeep6PjjH/8Y7r333s0K8o9//CN8/etfj5/zlBlPiDrA6mZMfqBAmwjQ7/ezzz4bW22U2iBjCnzta1+LT4lyfDPWIMdubho+fHg47LDDYqUoT4l+97vfjd1n5M7D/++44454buAagFYie+21V8XXGfnr8m8FFGh9AQKiHK8k7u+LJVqLjho1Kk5mEOb//d//jRWkufMTVP3GN74Rxynh/9wHFOtiJ3c5/6+AAi0vwPHMg0oEOUu17qRlJw9O3XfffTHoyfhihQIZHM9cC9DFFg8/cS+Qn3gg4tJLL43BFbrOov6AB6tMCmRJoCOF/VJDylKh66GsNJ3lyQ9OcJzoeMKDJnNc0HByY4wAbph4ioyuMU444YRA3+QmBRSobgGCGPQPTH/CtOJgEHSOY57g4AKJp0GoKGUaLy6euEHaaqutNqvIoHUYF0xUiHBOoPKEGykCplxwfe9734tPjnDe4ILqm9/8Zrwoqm4hc6dAfQoQEOEhhy9/+cuBgY8ZO+xjH/tYrNTgd73Yi5sjus3Iv4nhmoAbHLrHS11oEVTleKfbHc4JBF8IljAvN0+f/OQnPQfU59fLUtWAAA8wXHzxxbHlBq28ebqTp0F5p6XHG2+8EVt9cdwSzCBwSgtPfrtHjhy5WQnpF5zxRKZMmRL7C2ednCe4JqAFGRUkP/rRj+K1BBWhV1xxRTzfVPrwxWYb9AMFFGh1Ae4L7rzzznj/z8MM//mf/1l0mzwcNW7cuLBo0aIwfvz4uAz1B5xXuM+47rrrYtecTOdJ8+9///vxuqHoCp2ggAKtJsB9+s9+9rNw+eWXx/sAjtP04h7gzTffDE888US45pprwk9/+tP4e06X+ueff358qCE/Y7Qi4RzxwAMPxMHWuffn+KeugfsAWohw/ZBaix9zzDHxGsQuNPMl/bveBeLoOw2VZJW31653kRor31VXXRWuvfbaeMLkBMbFDydAboCoVOFpUQIi3/72t8OQIUNqrHRmV4HsCXBjwtg/VHZw4ULlBBdJVGRyquYzjvGU+Py9731v+MEPfhCnpc/T+w033BCfDqMChOV4opSKUp4aS13r8TQp5xL7EE1qvivQ9gIcjxdddFEcTJ0uL3hxvHLMl7pM45zBE2M84Z2feCiCFqNcJ3BDxTq33377eCNFKxGuEwiIvO9974tPilU6Zkn+dvxbAQWaL0ArkU984hOxooLjnkAFDzzwMARPkHK8EuCkyyuu78eMGRPnp/V4sUTw8ytf+Up82ILlWCfBEh6m4LqAawjOM5x7WE9+cLXYev1cAQXaR4Du7nhgguAm9/1UaJZKv//97+M1/iuvvBLvKXhynG4504MWPJCx3377xVajdOFlUkCB9hHgWKRFFw9H8GAU1+xDhw6NDz9wTUDdAL/bBDUYO5Budc8+++xw2mmnFawDoBSsk5aiP/7xj2MghHXy0CR1AqyLewjWRX0h1wpMMymQNQGDIjW+xzkp0lcwT4zQDyk3S3zGxQ59g7/73e8Op5xySjyh1nhRzb4CmRCg0oObHC58qPSgMpTASHrxd24FKS3FPvCBD8T+grlgKpR4Soy+hh9//PFARSjL0EUOzeoZn4hzhOMNFZLzMwXaToCucz772c/GoAiBCo7z1LqrVC74zad12aGHHlpwNgIj9CVMKxRanHGdwE0ST5cxDgHXCSeeeGKsKC24Aj9UQIE2EaCSk9adVHrSwoMKC1p2EsDg950HGqggoSKEysvDG7rMyh9LpFBGaVVKxehf/vKX2J0mAVgqRGh1ynpoJUZ3ewZECun5mQLVJfDggw+GM844IwZJuXafMGFC2QxSR8D4ggRQOB9wTuF8QhebnEe4Dyj0YEXZFTuDAgq0mAAPP/BwxG9+85vYEwTHKi1FuDbgWp97Ax5yplUX3V0eeeSRYe+99y67fe77uQ+ghSn3ATwYyf0FrUg47o899th4HUDA1KRAFgUMitTJXuemiSc+qPzghMrNDoERTm5UrJoUUKA2BKis5IKFi5VKEsf7oEGDYiVJqS4vqASh+40lS5bEcwRPhfTr1y8+IWpFSCXSzqNA6wpww0N3eARHigU4C+WAcwZPjJcbC4TrBJrLc51AwCVdJ/DUuE3lC8n6mQLtI8DDEYwbwsMRdJvJMc7vO7/btBqhyywqRhpzfU+lCAFR1kvrEM4x6SlU7hdMCihQGwKcH2j1wXmBSlK6v60k8UQ43W3yzrKcT7huoK6A84pJAQWqQ4B7dX6rOVZp4cn9Aff7/OZzzKdrgMbev6f6QoKiufcB1hdWx343F+0nYFCk/ezdsgIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCrShQOG+VtowA25KAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFGgLAYMibaHsNhRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUKDdBQyKtPsuMAMKKKCAAgoooIACCiiggAIKKKCAAgoooIACCijQFgIGRdpC2W0ooIACCiiggAIKKKCAAgoooIACCiiggAIKKKBAuwsYFGn3XWAGFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQoC0EDIq0hbLbUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAgXYXMCjS7rvADCiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooEBbCBgUaQtlt6GAAgoooIACCiiggAIKKKCAAgoooIACCiiggALtLmBQpN13gRlQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUECBthAwKNIWym5DAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEF2l3AoEi77wIzoIACCiiggAIKKKCAAgoooIACCiiggAIKKKCAAm0hYFCkLZTdhgIKKKCAAgoooIACCiiggAIKKKCAAgoooIACCrS7gEGRdt8FZkABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQXaQsCgSFsouw0FFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRodwGDIu2+C8yAAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKtIVAp7bYiNtQQAEFFFBAAQUUUEABBapNYOXKleGZZ54Jc+fODR06/Ot5sTVr1oR99tknDB8+vE2yvH79+rDFFlsU3FapaQUXaOaHr7/+enj11Vc38Vi7dm3YZpttwqhRo0KXLl2auQUXV0ABBRRQQAEFFFCgfQUMirSvv1tXQAEFFFBAAQUUyIjAwoULw9133x0WL15csAKcyuaePXuGoUOHhl122SUMHjw4IzLtV0z2yWWXXRYeeuih0Llz50DlP0GI1atXh5/97GfhzDPP3GRfrVq1Kjz22GPhpZdeCp06dYrz9uvXL5x22mkVFYJ1X3/99XE5FiD4MmbMmHDwwQdvXP65554LTz/9dJxn2223Dccdd9zGaYX+M3v27HDbbbfF/HTt2jUceOCBYbfddis0a/yMMk6YMCE8/vjjcRtHHnlkDHakBX7961+HL37xi9GDQE3Hjh3DihUrwllnnRW+9a1vhYEDB6ZZfVdAAQUUUEABBRRQoCYFDIrU5G4z0woooIACCiiggAK1JjBnzpzw1a9+NUyePDlWNFMhTiV5SlRo9+7dO2y99dZh5513Dscff3w4+eSTQ/fu3dMsvrewAJX+BKL69u0bK/uxJzi1fPnyGJzKb71By5Jbb701BjbYL+w/liewUEmwYPz48eH888/fGGhhOxdccMEmQRG+HxdffHEMmOyxxx5lgyIEdM4777zA94dAzRe+8IWSQZGlS5eGG2+8MfzoRz+Keef/tABJacSIEeHoo4+O66K8U6ZMiS1pevTosTHfaV7fFVBAAQUUUEABBRSoRQGDIrW418yzAgoooIACCiigQM0JpAr4Xr16hQEDBoQddtghBjxSYIQWJK+88kpshfDGG2/EJ/mnT58ePv3pT8fK65orcA1lmJYhI0eOjAEKWuoQsOK9UOrWrVsMXvFOInBw1113hf/4j/8oNPsmn910002B/c8+pwUGQQyCGbmJgESfPn0CrVIWLFgQZsyYUTQvLPfII4+ELbfcMnZ3Rb7/8Y9/hHXr1m3S/VXu+pctWxaef/75GAgiGML3MDe95z3vCQcccEBcnoDIFVdcEWbNmpU7i/9XQAEFFFBAAQUUUKCmBQyK1PTuM/MKKKCAAgoooIACtSZABfxOO+0UuyjabrvtYpdNlIFujebPnx9+8YtfxJYIS5YsCWPHjo1P/b/3ve+ttWLWVH5Tiw/2y5AhQ8rmPc2fxiGhq61yQRFahfzlL3+JwRC6RiPYUSgRNKGl0Isvvhi78fq///u/cNJJJxWaNX5GUITgCq2MaI301ltvxcAILT4KJboMY90E6XbcccfNAi4E7HiR6FKMoE0K3BVan58poIACCiiggAIKKFBrAv8aTbDWcm5+FVBAAQUUUEABBRSoUQG6aKJinBctEngxqPeee+4ZLrroovDxj388tkCgq6Pbb789MG6EqXUFqPinhUVj0jHHHBNb+7z22mvhhRdeKLko+5H9SSCFbtFoCVIsvfOd74yBCFqhEBQplgiAzJw5M+aBdfK94rM333yz4CJpPBECc5SVoEipIFBjPQpu1A8VUEABBRRQQAEFFKgyAYMiVbZDzI4CCiiggAIKKKBANgSKPX3PU/r/9m//FgYNGhQr0Bl0e968edlAqaFSErD4wAc+sLH7q/TjBwUAABUBSURBVF/+8pclc88A5gQZ2L8Mnk5golhKQRHmYeD1Yulvf/tbDJ4QDCFAs+uuu8ZWIoxLUiixvieeeCK2VqHLLYIiJgUUUEABBRRQQAEFsiZgUCRre9zyKqCAAgoooIACClS9AIN205UTgRO6WaLrpfy0aNGicPPNN4dTTz01DpS97bbbxnEx3v/+94ef/vSnBZdJ6zjxxBND//7943gSxVoDfO1rXwtbbbVVDM6wbsarKJS+9a1vxdYGDDxOAKdYsIfxURjcm67Adt999zigPC1jTjvttEBAYcWKFYVWHz9j21T49+vXL7ayIM+MwXLLLbfEABIDkpPXo446qug6WnoCrS522223sNdee8WuqO68886im3j11VcD48SQbwIptBYp5sRKDj744I3zEBBjnJBCia6z6AaLQdAPOeSQgAPdrrGtQi1RCIrQ8oTxTBhUPn88kULb8DMFFFBAAQUUUEABBepNwKBIve1Ry6OAAgoooIACCihQ8wKMEUGQgUSwIDdwQWX6+PHjYwDgc5/7XHj00UdjRTjL0D0TFeWf//znY8X6k08+WdCCoARjUDCQ90svvbTZPFSoT5o0KY5zQisEKtoZnDs/EaxgPlpNMD4KgRwq6XMT62KclCOOOCJccsklMe/kk7EqCOw89NBDgXIQLCnVKoLy8WKcC8bEOOOMM8JnPvOZGIhhfQx8TmV/WybKTT4IcpCHO+64o+Dm6TqLAc6xOf3000u2EmEF7HsCRuxr7CdMmFBwvexftr3vvvvG8o8ePTrON3HixIJjluCdxhMZNmyYQZGCqn6ogAIKKKCAAgooUO8CBkXqfQ9bPgUUUEABBRRQQIGaE6CynYAFlegELwgGpPTMM8+EU045JbYeoOXEmDFjwkc+8pFw4YUXho9+9KOx5QLLMNYEFfAPP/xwWnTj+3777Rd69uwZgwgPPPDAxs/Tf2hpMG3atDidPLD9Bx98ME3e+E5AZOrUqTFoc9BBB8WAxcaJDf8hIHL99deHCy64IDDANy0Tjj322PDf//3fMXDD2CkHHnhgbI3y1FNPhXPPPbdg8CWtkwAA+briiitiMIRupv7rv/4rrov3kSNHplnb5J2WFwRz+vTpE1vS3HbbbQW3O27cuBgUIXjBAOi0MimXDjvssDgfLWIKBUVoPcLA6uwfWomQKD/79fXXX4/7P3cbBFjoOovEMttss01sXZM7j/9XQAEFFFBAAQUUUCALAv+6u8pCaS2jAgoooIACCiiggAI1IMCYEAzeTaLrKiq6SbQaOeecc2LXWLQm+OAHPxguu+yy0KtXrzidfwioXH755eHnP/95nP/SSy8Nf/jDH2J3WWkmgiIsT8sKgh1nn312mhTfCXZMmTIl9O3bNy5H4OOvf/1rOP/88zeZj3wyjUp+unyiVUlKVMLTkoHutfg/AYz/+Z//Cfvss0+aZeP7ddddF77zne8EAj433XRT+PKXvxyDQRtn+Od/CIrQCoKuuAi0nHfeeW3eOiQ3T5SLAMP73ve+cOONN8bgBUEbuqZK6f777w9vv/12DBCdddZZ8WOWKZcOPfTQaEfXaa+88spmsz/22GMbu+BKQRHGK6FLr7///e8xeJS/EGOQEOAiiEN3ZCYFFFBAAQUUUEABBbIoYEuRLO51y6yAAgoooIACCijQrgJUiucGEHIzQ4U2rSuoDKelBZXjDLpOohsmxqdgWQIbX/nKVzYJiDAP3VJdddVVsQUG3W699dZbsfsqpqVEV1dpIHe6rKLFQ25KLUXojokBvFkn3Wflj/tB8IQABV1aEezgPSW6faIcs2bNil1BkddCARHm/9jHPhZbfOBy3333hZdffjmtZrN3AiMMRE+rkrbuLmuzzPzzgzPPPDMGKOgi63e/+90msxEUmTt3bhxgnVYyBFIqSXvvvXccK4R5Z8+eHbspy12OoAjBKFoL7bzzznESQRFai6RASu7+wpau1vBjvlGjRuWuzv8roIACCiiggAIKKJAZAYMimdnVFlQBBRRQQAEFFFCgGgSolJ4/f37s1orxNGiBwevuu+8OY8eODf/v//2/2HqDvNLd1Mknnxyf7OfvW2+9NQYeGD+DsSyKBVaY9+KLL45BFSrqqQzPTymIQQU6g2+nxLgTtMbgc4Iixx13XNwmLVBS90vMS9CDbpqoeGcskSFDhqRVxHe6drrnnntii4/3vOc9JSvhMWE7Q4cODbQ+Yb2FEkEeBlQnuJDbpVihedvyM8ZoYQwQTB7M6WaMgBHBJD4/6aSTCrZ+KZZPWvKwjygzQRGCYbmJ9TKmywEHHLDx49RShA8YO4TvWUozZswIb775ZmzZQiCFQdlNCiiggAIKKKCAAgpkUcDus7K41y2zAgoooIACCiigQLsJ0LqByulvfvObm1TsE3SgVQUDdpPo3ohusPbff//4Ny0MqBhPrUyOPvro+Hmxfxi/gtYgjE1CSwW6uWIciZTocokgCxXrBGfe8Y53xElU5BOUYDsERej2iu67GNuC4M3hhx8e56OSPXXxReV96uKLiVTkU2lPWQYPHhy7j/rTn/4Uu/aKC+f9gwljZBAIIJFXgjLp7zQ7BgRFCEJUW6K1CF2bUQ5aceDLeC78Tb4/9KEPNSrL+GNPl1cEmNj3BJdItOQh4EFLEeZJCUcCaQQ98GffE2gipe62mGf48OFhyy23TIv5roACCiiggAIKKKBApgQMimRqd1tYBRRQQAEFFFBAgfYWoFUELQcYZyJ3wO0ePXrEbo148v+II46IFeC0wKASm0TAhO60SLQUYbyPUomWFDvuuGNgAHNac1CxnhsUoTI9rZvWDZdccklcHcEOxrBg3jT/XnvtFfObO2g7A7lTUU8AhKAI41SkRBCArrXoTouuue68885w7733Fu06igAAwRlcWIZB2VmuUFCEz2gRUW3phBNOCIzfwn79y1/+EoMitMDBiQAVXVxRTmwqSSkoQrdjfGfwxJr/s08JomFG92q5iYAHg7m/8MILsWuzNPj8I488Evc33zP2p0kBBRRQQAEFFFBAgawKGBTJ6p633AoooIACCiiggALtIkBgg0HJGXScsT1SYITKboIUjN9BqwvecxMtNVLKHVg9fZb/TuU7gRPeCTCkFihpPloKUHlON0sMqk6rAj6jFQsV+bRKGDhwYJz9Xe96V2BsDMYnIbjC57SAYL7evXvHwb1zu7Nim7RkoEyUj+6fqMSnor9UYjrdfVFWAgCFEtvJtyk0X1t/hh2BEVrfMC7MuHHjwksvvRTLfcopp2wcH6TSfGFBIAVrgkXsI8zZZwRFcBo2bFhsGZK7Tj7bfvvtA2PFTJgwIbYAIohGUIT9wXdrzJgxuYv4fwUUUEABBRRQQAEFMiVgUCRTu9vCKqCAAgoooIACCrS3AAEDKqmpvN56660rzk5uICB3AO1SK0iBiBRsyZ+XLrMIihCoYbyQww47LLYwICBBF1UpKEJrBCrpaZlAN0yM/0H3TJSFbr7oIis/kV8CInTlRCsUuo+qJN+sk9YipcZLIS/VlgjWUMaf//znMbB0zTXXxJY0BEuOOuqokuUpVhZaxdDKhG7LCEgRGCEoQkseuhd797vfvdmifKcIimD09NNPx6DMvHnzYgCLz2gpwjpNCiiggAIKKKCAAgpkVcCB1rO65y23AgoooIACCiigQE0JEKDgSX8SrQQIHpRLVKJTEU6AIbd7q7QcLUCYnoIdtNCg2yXm32233TaOE0I3XgQ30ny0XCAoQho1alQcuyStk3fWSaCEPBIIIehCYIZWCuVetIIhoMI6aimRXwYv50WLjscffzwwaP0xxxxTMGhUSdnYD3SnRpCKcVamTZsW102rHgJOaRyY3HWRDwInBGPGjx8fWwk9+uijcV/w/WFafrdkucv7fwUUUEABBRRQQAEF6l3AoEi972HLp4ACCiiggAIKKFAXAlRkp0Gz6Q6L8SpKJcYGoSssKsKpIGcA7vxEpTrTqWCnSyZaFBAUYSySbbfddpPZaS3CdtN8tDChAp4xK1KLkrQA62ScEYIiVOBPnDgxTarrdwI6J554YmydQdnxOvbYY5s8BgpBkf333z/uHwJLkydPDgQ4CLYQKGEw90KJ/UeLkdTFGYO1s69ogXPggQcWWsTPFFBAAQUUUEABBRTIjIBBkczsaguqgAIKKKCAAgooUOsCVLBTGU7XSYxdUSpde+21sXVGavVBhXh+Ytro0aPjOqdPnx5+//vfx/E8aBmSBllPy9C1FttmwPff/e53sWKeYAgV8PmJCniCMLSaIDhAqwlaLVSaWKYWE4ErxhXZvqH7KgY8J+jE+B14NCWxHOPODBkyJK7jjTfeiGO7EGhK3WQVWi/7hGXY5wREeJHo4ovxbEwKKKCAAgoooIACCmRZwKBIlve+ZVdAAQUUUEABBRSoKYGzzjorDmxOq4E///nP4Y9//GPB/D/wwAPhhhtuiJXggwYNCqeffnrB+fjwiCOOiMEOBkb/5S9/GbuuolKdSv3clLraWrhwYfjFL34R56Mrpvz50jK0mvjUpz4V88Cg34yxwVgYxRIBF7r7ogJ/7ty5xWar+s/32muvOAYI+4bAFd2QNSfhmAZGp8syWgjRfRoBl2LBFoIytCoi6PWb3/wmjidCHgja0ILHpIACCiiggAIKKKBAlgUcaD3Le9+yK6CAAgoooIACCtSUAK03zjnnnPDVr341Bg4YwHzSpElx4GwG0KayfMKECeHqq6+OXTelwb/33nvvouU8/PDDw5VXXhmDF3PmzIljftBShGVzEy0PCILQhRNBC4IY/J3fzVZahlYK73vf++LA7LRA+e1vfxvHxDj11FMD6+/du3fsuotWL3TbRSsIgjm0RBk7duxmXXKl9Vb7O12HMf5KSyWc2H/33ntvHFeEQAhjuxx00EEbx5gptC0CWwRU6A4ttRIiQJO/Xwst62cKKKCAAgoooIACCtSzwKZ3OvVcUsumgAIKKKCAAgoooEAdCND6ggHRr7vuuhgQufzyy2Nrjf79+wdaezCOBIlxRD7xiU/EIEqpYtN9FvMyIDpp2LBhMWhRaBlalVx//fWxBQIDp9NFVqkAANO++MUvxsp5giIPPfRQbAlCcIf8EkBg0PaZM2fGQAtjm9C9E5X5pg0CBLvohoxgCOYp7bfffpv8nT5P76NGjYr7lXFNSCxPax+TAgoooIACCiiggAJZFzAokvVvgOVXQAEFFFBAAQUUaBMBWlYwQDbjQRDUIADQlETF+IUXXhj23HPP8Kc//Sk8+eSTsfUGLS2oQN95553j4Ny00jj66KNjAKPUdmhFQPdMd955Z5yN9e6yyy4FF2Fckauuuip2nTVgwICCg7fnL0iQhcAIy44bNy4O1E5rE4I3jB1CAITxMaiwp2sngiKMo5GfcCPog117JfK7dOnSuA9XrlzZ5H1IoIJgEInWPeUShgSYGPeFROscWu6USuxHWoXQCofgE69DDz201CJOU0ABBRRQQAEFFFAgEwIGRTKxmy2kAgoooIACCiigQHsLUPHPmB1UglPBzd9NTVR2M6A3AYS33347VrATZCFgQpCBcUQYBL3S9M1vfjOcd955cXZacBSrcCdw8fDDD8cK9q5duxYcZL3QNvv06RO70mJ5usciIMC4KCTKQjCnb9++Md+Mg5GfKNftt98el6FVCxX87ZF69uwZLrroonDmmWdG62Jdh5XKGy02CELRaoZyVPI9GDlyZKALshRAYR+zn0olWuMwrgzWbJNE6xGTAgoooIACCiiggAJZFzAokvVvgOVXQAEFFFBAAQUUaBMBBrned999W3RbVIyXqxyvZIOMP8GrXCIo0JwumAho8GpsolL/gAMOaOxijZqfFjPluu0iOMN4KLyakwgANabVBu5psPVKt0teGfS9OQmP9gpANSffLquAAgoooIACCiigQCkBgyKldJymgAIKKKCAAgoooIACdS9A65S//vWvMfBCEIAWGd/97nfDySefXPdlzy/gD3/4w/D9738/tuChyzcs0kDt+fP6twIKKKCAAgoooIACtShgUKQW95p5VkABBRRQQAEFFFBAgRYToCXKmjVrNo5XQiCA7r0YQyR1PdViG6vyFa1YsSKOmWIgpMp3lNlTQAEFFFBAAQUUaLKAQZEm07mgAgoooIACCiiggAIK1LIAY5m8//3vjwPT091USgREGMcjawERyk83ZWeffXYcMyV5MDD83nvvHegCzqSAAgoooIACCiigQK0LxBH3Gp6AWl/rBTH/CiiggAIKKKCAAgoooIACCiiggAIKKKCAAgoooEApgQ6lJjpNAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFKgXAYMi9bInLYcCCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgqUFDAoUpLHiQoooIACCiiggAIKKKCAAgoooIACCiiggAIKKFAvAgZF6mVPWg4FFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRQoKWBQpCSPExVQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUKBeBAyK1MuetBwKKKCAAgoooIACCiiggAIKKKCAAgoooIACCihQUsCgSEkeJyqggAIKKKCAAgoooIACCiiggAIKKKCAAgoooEC9CBgUqZc9aTkUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFFFCgpIBBkZI8TlRAAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQIF6ETAoUi970nIooIACCiiggAIKKKCAAgoooIACCiiggAIKKKBASQGDIiV5nKiAAgoooIACCiiggAIKKKCAAgoooIACCiiggAL1ImBQpF72pOVQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQUUUECBkgIGRUryOFEBBRRQQAEFFFBAAQUUUEABBRRQQAEFFFBAAQXqRcCgSL3sScuhgAIKKKCAAgoooIACCiiggAIKKKCAAgoooIACJQUMipTkcaICCiiggAIKKKCAAgoooIACCiiggAIKKKCAAgrUi4BBkXrZk5ZDAQUUUEABBRRQQAEFFFBAAQUUUEABBRRQQAEFSgoYFCnJ40QFFFBAAQUUUEABBRRQQAEFFFBAAQUUUEABBRSoFwGDIvWyJy2HAgoooIACCiiggAIKKKCAAgoooIACCiiggAIKlBT4/8LwJmbKwW1YAAAAAElFTkSuQmCC", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "# this cell is used to display the image in the notebook when using colab\n", "# or running the notebook locally\n", @@ -202,9 +213,12 @@ "source": [ "# Initialize the Electrolyser class with core attributes\n", "\n", + "from datetime import datetime\n", + "\n", "import pandas as pd\n", "\n", "from assume.common.base import BaseStrategy, SupportsMinMax\n", + "from assume.common.forecasts import Forecaster\n", "from assume.common.market_objects import MarketConfig, Order, Orderbook, Product\n", "\n", "\n", @@ -213,13 +227,13 @@ " self,\n", " id: str,\n", " technology: str,\n", - " index: pd.DatetimeIndex,\n", " unit_operator: str,\n", " bidding_strategies: str,\n", " max_power: float,\n", " min_power: float,\n", " max_hydrogen: float,\n", " min_hydrogen: float,\n", + " forecaster: Forecaster,\n", " additional_cost: float,\n", " **kwargs,\n", " ):\n", @@ -228,7 +242,7 @@ " unit_operator=unit_operator,\n", " technology=technology,\n", " bidding_strategies=bidding_strategies,\n", - " index=index,\n", + " forecaster=forecaster,\n", " **kwargs,\n", " )\n", "\n", @@ -246,30 +260,28 @@ " # and is executed after each market clearing\n", " def execute_current_dispatch(\n", " self,\n", - " start: pd.Timestamp,\n", - " end: pd.Timestamp,\n", + " start: datetime,\n", + " end: datetime,\n", " ):\n", - " end_excl = end - self.index.freq\n", - "\n", " # Calculate mean power for this time period\n", - " avg_power = abs(self.outputs[\"energy\"].loc[start:end_excl]).mean()\n", + " avg_power = abs(self.outputs[\"energy\"].loc[start:end]).mean()\n", "\n", " # Decide which efficiency point to use\n", " if avg_power < self.min_power:\n", - " self.outputs[\"energy\"].loc[start:end_excl] = 0\n", - " self.outputs[\"hydrogen\"].loc[start:end_excl] = 0\n", + " self.outputs[\"energy\"].loc[start:end] = 0\n", + " self.outputs[\"hydrogen\"].loc[start:end] = 0\n", " else:\n", " if avg_power <= 0.35 * self.max_power:\n", " dynamic_conversion_factor = self.conversion_factors[0]\n", " else:\n", " dynamic_conversion_factor = self.conversion_factors[1]\n", "\n", - " self.outputs[\"energy\"].loc[start:end_excl] = avg_power\n", - " self.outputs[\"hydrogen\"].loc[start:end_excl] = (\n", + " self.outputs[\"energy\"].loc[start:end] = avg_power\n", + " self.outputs[\"hydrogen\"].loc[start:end] = (\n", " avg_power / dynamic_conversion_factor\n", " )\n", "\n", - " return self.outputs[\"energy\"].loc[start:end_excl]\n", + " return self.outputs[\"energy\"].loc[start:end]\n", "\n", " # this function is a must be part of each unit class\n", " # as it dictates which parameters of the unit we would like to save to the database\n", @@ -334,12 +346,12 @@ "class Electrolyser(Electrolyser):\n", " def calculate_min_max_power(\n", " self,\n", - " start: pd.Timestamp,\n", - " end: pd.Timestamp,\n", - " hydrogen_demand=0,\n", + " start: datetime,\n", + " end: datetime,\n", " ):\n", " # check if hydrogen_demand is within min and max hydrogen production\n", " # and adjust accordingly\n", + " hydrogen_demand = self.forecaster[\"hydrogen_demand\"][start]\n", " if hydrogen_demand < self.min_hydrogen:\n", " hydrogen_production = self.min_hydrogen\n", "\n", @@ -355,7 +367,20 @@ " )\n", " power = hydrogen_production * dynamic_conversion_factor\n", "\n", - " return power, hydrogen_production" + " return power, hydrogen_production\n", + "\n", + " def calculate_marginal_cost(self, start: datetime, power: float = 0) -> float:\n", + " \"\"\"\n", + " Calculate the marginal cost of the unit returns the marginal cost of the unit based on the provided time and power.\n", + "\n", + " Args:\n", + " start (pandas.Timestamp): The start time of the dispatch.\n", + " power (float): The power output of the unit.\n", + "\n", + " Returns:\n", + " float: the marginal cost of the unit for the given power.\n", + " \"\"\"\n", + " return self.forecaster[\"fuel_price\"].at[start]" ] }, { @@ -479,15 +504,13 @@ "\n", " # Get hydrogen demand and price for the product start time\n", " # in this case for the start hour of the product\n", - " hydrogen_demand = unit.forecaster[f\"{unit.id}_h2demand\"].loc[start]\n", - " hydrogen_price = unit.forecaster[f\"{unit.id}_h2price\"].loc[start]\n", + " hydrogen_price = unit.calculate_marginal_cost(start=start)\n", "\n", " # Calculate the required power and the actual possible hydrogen production\n", " # given the hydrogen demand\n", " power, hydrogen_production = unit.calculate_min_max_power(\n", " start=start,\n", " end=end,\n", - " hydrogen_demand=hydrogen_demand,\n", " )\n", "\n", " # Calculate the marginal revenue of producing hydrogen\n", @@ -540,7 +563,7 @@ "from dateutil import rrule as rr\n", "\n", "from assume import World\n", - "from assume.common.forecasts import CsvForecaster, NaiveForecast\n", + "from assume.common.forecasts import NaiveForecast\n", "from assume.common.market_objects import MarketConfig, MarketProduct\n", "\n", "logger = logging.getLogger(__name__)\n", @@ -646,8 +669,12 @@ "\n", "# add the electrolyser unit to the world\n", "world.add_unit_operator(id=\"electrolyser_operator\")\n", - "hydrogen_plant_forecaster = CsvForecaster(index=index)\n", - "hydrogen_plant_forecaster.set_forecast(data=hydrogen_forecasts)\n", + "\n", + "hydrogen_plant_forecaster = NaiveForecast(\n", + " index=index,\n", + " demand=hydrogen_forecasts[\"electrolyser_01_h2demand\"],\n", + " fuel_cost=hydrogen_forecasts[\"electrolyser_01_h2price\"],\n", + ")\n", "\n", "world.add_unit(\n", " id=\"electrolyser_01\",\n", @@ -663,8 +690,15 @@ " \"additional_cost\": 10,\n", " },\n", " forecaster=hydrogen_plant_forecaster,\n", - ")\n", - "\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ "# run the simulation\n", "world.run()" ] @@ -777,7 +811,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.7" + "version": "3.12.6" }, "nbsphinx": { "execute": "never" diff --git a/examples/notebooks/04_reinforcement_learning_example.ipynb b/examples/notebooks/04_reinforcement_learning_example.ipynb index d85f4b334..847faf586 100644 --- a/examples/notebooks/04_reinforcement_learning_example.ipynb +++ b/examples/notebooks/04_reinforcement_learning_example.ipynb @@ -793,7 +793,7 @@ " # if we are not in learning mode we just use the actor neuronal net to get the action without adding noise\n", "\n", " curr_action = self.actor(next_observation).detach()\n", - " noise = tuple(0 for _ in range(self.act_dim))\n", + " noise = th.zeros(self.act_dim, dtype=self.float_type)\n", "\n", " curr_action = curr_action.clamp(-1, 1)\n", "\n", @@ -2020,7 +2020,7 @@ " # if we are not in learning mode we just use the actor neuronal net to get the action without adding noise\n", "\n", " curr_action = self.actor(next_observation).detach()\n", - " noise = tuple(0 for _ in range(self.act_dim))\n", + " noise = th.zeros(self.act_dim, dtype=self.float_type)\n", "\n", " curr_action = curr_action.clamp(-1, 1)\n", "\n", @@ -2571,7 +2571,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.12.7" } }, "nbformat": 4, diff --git a/examples/world_script.py b/examples/world_script.py index d7963d11d..3560d454b 100644 --- a/examples/world_script.py +++ b/examples/world_script.py @@ -5,10 +5,10 @@ import logging from datetime import datetime, timedelta -import pandas as pd from dateutil import rrule as rr from assume import World +from assume.common.fast_pandas import FastIndex from assume.common.forecasts import NaiveForecast from assume.common.market_objects import MarketConfig, MarketProduct @@ -18,11 +18,8 @@ def init(world, n=1): start = datetime(2019, 1, 1) end = datetime(2019, 3, 1) - index = pd.date_range( - start=start, - end=end + timedelta(hours=24), - freq="h", - ) + + index = FastIndex(start, end, freq="h") sim_id = "world_script_simulation" world.setup( @@ -30,13 +27,14 @@ def init(world, n=1): end=end, save_frequency_hours=48, simulation_id=sim_id, - index=index, ) marketdesign = [ MarketConfig( market_id="EOM", - opening_hours=rr.rrule(rr.HOURLY, interval=24, dtstart=start, until=end), + opening_hours=rr.rrule( + rr.HOURLY, interval=24, dtstart=start, until=end, cache=True + ), opening_duration=timedelta(hours=1), market_mechanism="pay_as_clear", market_products=[MarketProduct(timedelta(hours=1), 24, timedelta(hours=1))], diff --git a/examples/world_script_policy.py b/examples/world_script_policy.py index 2fe93fd77..b78f5c590 100644 --- a/examples/world_script_policy.py +++ b/examples/world_script_policy.py @@ -40,7 +40,6 @@ def init(world: World): end=end, save_frequency_hours=48, simulation_id=sim_id, - index=index, ) marketdesign = [ diff --git a/tests/conftest.py b/tests/conftest.py index a8fbddbc3..1ae381cf3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,34 +4,39 @@ from datetime import datetime +import numpy as np import pandas as pd import pytest from assume.common.base import SupportsMinMax +from assume.common.fast_pandas import FastSeries +from assume.common.forecasts import NaiveForecast class MockMarketConfig: market_id = "EOM" + maximum_bid_price = 3000.0 + minimum_bid_price = -500.0 product_type = "energy" additional_fields = [] class MockMinMaxUnit(SupportsMinMax): - def __init__(self, index, **kwargs): - super().__init__("", "", "", {}, index, None, **kwargs) + def __init__(self, forecaster, **kwargs): + super().__init__("", "", "", {}, forecaster, None, **kwargs) self.max_power = 1000 self.min_power = 0 self.ramp_down = 200 self.ramp_up = 400 def calculate_min_max_power( - self, start: pd.Timestamp, end: pd.Timestamp, product_type="energy" - ) -> tuple[pd.Series, pd.Series]: - min = pd.Series(100, self.index).loc[start:start] - max = pd.Series(400, self.index).loc[start:start] + self, start: datetime, end: datetime, product_type="energy" + ) -> tuple[np.array, np.array]: + min = FastSeries(value=100, index=self.index).loc[start:end] + max = FastSeries(value=400, index=self.index).loc[start:end] return min, max - def calculate_marginal_cost(self, start: pd.Timestamp, power: float) -> float: + def calculate_marginal_cost(self, start: datetime, power: float) -> float: return 3 @@ -43,8 +48,7 @@ def mock_market_config(): @pytest.fixture def mock_supports_minmax(): index = pd.date_range( - start=datetime(2023, 7, 1), - end=datetime(2023, 7, 2), - freq="1h", + start=datetime(2023, 7, 1), end=datetime(2023, 7, 2), freq="1h" ) - return MockMinMaxUnit(index) + forecaster = NaiveForecast(index, demand=150) + return MockMinMaxUnit(forecaster=forecaster) diff --git a/tests/test_advanced_order_strategy.py b/tests/test_advanced_order_strategy.py index dd02b952e..6378f95c5 100644 --- a/tests/test_advanced_order_strategy.py +++ b/tests/test_advanced_order_strategy.py @@ -27,7 +27,6 @@ def power_plant() -> PowerPlant: id="test_pp", unit_operator="test_operator", technology="coal", - index=index, max_power=1000, min_power=200, efficiency=0.5, diff --git a/tests/test_baseunit.py b/tests/test_baseunit.py index 9b79a8200..ea7dfd26c 100644 --- a/tests/test_baseunit.py +++ b/tests/test_baseunit.py @@ -34,11 +34,11 @@ def calculate_bids( return bids -@pytest.fixture(params=["h", "15min"]) +@pytest.fixture(params=["1h", "15min"]) def base_unit(request) -> BaseUnit: # Create a PowerPlant instance with some example parameters index = pd.date_range("2022-01-01", periods=4, freq=request.param) - NaiveForecast( + forecaster = NaiveForecast( index, availability=1, fuel_price=[10, 11, 12, 13], co2_price=[10, 20, 30, 30] ) return BaseUnit( @@ -46,7 +46,8 @@ def base_unit(request) -> BaseUnit: unit_operator="test_operator", technology="base", bidding_strategies={"EOM": BasicStrategy()}, - index=index, + forecaster=forecaster, + index=forecaster.index, ) diff --git a/tests/test_data_request_mechanism.py b/tests/test_data_request_mechanism.py index 5a499e5b4..658d447a5 100644 --- a/tests/test_data_request_mechanism.py +++ b/tests/test_data_request_mechanism.py @@ -75,16 +75,16 @@ async def test_request_messages(): units_agent.add_role(units_role) index = pd.date_range(start=start, end=end + pd.Timedelta(hours=4), freq="1h") - + forecaster = NaiveForecast(index, demand=1000) params_dict = { "bidding_strategies": {"EOM": NaiveSingleBidStrategy()}, "technology": "energy", "unit_operator": "test_operator", "max_power": 1000, "min_power": 0, - "forecaster": NaiveForecast(index, demand=1000), + "forecaster": forecaster, } - unit = Demand("testdemand", index=index, **params_dict) + unit = Demand("testdemand", index=forecaster.index, **params_dict) units_role.add_unit(unit) market_role = MarketRole(marketconfig) diff --git a/tests/test_demand.py b/tests/test_demand.py index bf5a808fb..0032bf234 100644 --- a/tests/test_demand.py +++ b/tests/test_demand.py @@ -26,21 +26,20 @@ def test_demand(): datetime(2023, 7, 1, hour=2), None, ) - ff = NaiveForecast(index, demand=150) + forecaster = NaiveForecast(index, demand=150) dem = Demand( - "demand_test01", - "UO1", - "energy", - strategies, - index, - 150, - 0, - forecaster=ff, + id="demand", + unit_operator="UO1", + technology="energy", + bidding_strategies=strategies, + max_power=150, + min_power=0, + forecaster=forecaster, price=2000, ) start = product_tuple[0] end = product_tuple[1] - min_power, max_power = dem.calculate_min_max_power(start, end) + _, max_power = dem.calculate_min_max_power(start, end) assert max_power.max() == -150 assert dem.calculate_marginal_cost(start, max_power.max()) == 2000 @@ -82,21 +81,20 @@ def test_demand_series(): price = pd.Series(1000, index=index) price.iloc[1] = 0 - ff = NaiveForecast(index, demand=demand) + forecaster = NaiveForecast(index, demand=demand) dem = Demand( - "demand_test02", - "UO1", - "energy", - strategies, - index, - 150, - 0, - forecaster=ff, + id="demand", + unit_operator="UO1", + technology="energy", + bidding_strategies=strategies, + max_power=150, + min_power=0, + forecaster=forecaster, price=price, ) start = product_tuple[0] end = product_tuple[1] - min_power, max_power = dem.calculate_min_max_power(start, end) + _, max_power = dem.calculate_min_max_power(start, end) # power should be the highest demand which is used throughout the period # in our case 80 MW diff --git a/tests/test_dmas_powerplant.py b/tests/test_dmas_powerplant.py index 8cec44e04..2def19b1f 100644 --- a/tests/test_dmas_powerplant.py +++ b/tests/test_dmas_powerplant.py @@ -33,7 +33,6 @@ def power_plant_1() -> PowerPlant: unit_operator="test_operator", technology="hard coal", bidding_strategies={"EOM": DmasPowerplantStrategy()}, - index=index, max_power=1000, min_power=200, efficiency=0.5, @@ -63,7 +62,6 @@ def power_plant_day(fuel_type="lignite") -> PowerPlant: unit_operator="test_operator", technology="hard coal", bidding_strategies={"EOM": DmasPowerplantStrategy()}, - index=index, max_power=1000, min_power=200, efficiency=0.5, diff --git a/tests/test_dmas_storage.py b/tests/test_dmas_storage.py index 5b843f504..e4d08f988 100644 --- a/tests/test_dmas_storage.py +++ b/tests/test_dmas_storage.py @@ -13,25 +13,28 @@ from assume.common.utils import get_available_products from assume.strategies.dmas_storage import DmasStorageStrategy from assume.strategies.naive_strategies import NaiveSingleBidStrategy -from assume.units import PowerPlant, Storage +from assume.units import Storage from .utils import get_test_prices @pytest.fixture def storage_unit() -> Storage: + index = pd.date_range("2022-01-01", periods=4, freq="h") + forecaster = NaiveForecast(index, availability=1, price_forecast=50) + return Storage( id="Test_Storage", unit_operator="TestOperator", technology="TestTechnology", bidding_strategies={"EOM": NaiveSingleBidStrategy()}, + forecaster=forecaster, max_power_charge=100, max_power_discharge=100, max_soc=1000, initial_soc=500, efficiency_charge=0.9, efficiency_discharge=0.95, - index=pd.date_range("2022-01-01", periods=4, freq="h"), ramp_down_charge=-50, ramp_down_discharge=50, ramp_up_charge=-60, @@ -42,7 +45,7 @@ def storage_unit() -> Storage: @pytest.fixture -def storage_day() -> PowerPlant: +def storage_day() -> Storage: periods = 48 index = pd.date_range("2022-01-01", periods=periods, freq="h") @@ -64,7 +67,6 @@ def storage_day() -> PowerPlant: initial_soc=500, efficiency_charge=0.9, efficiency_discharge=0.95, - index=index, ramp_down_charge=-50, ramp_down_discharge=50, ramp_up_charge=-60, @@ -79,8 +81,6 @@ def test_dmas_str_init(storage_unit): strategy = DmasStorageStrategy() hour_count = len(storage_unit.index) - prices = get_test_prices() - strategy.build_model( storage_unit, datetime(2022, 1, 1), diff --git a/tests/test_drl_storage_strategy.py b/tests/test_drl_storage_strategy.py index 9267a55ec..53583fbb7 100644 --- a/tests/test_drl_storage_strategy.py +++ b/tests/test_drl_storage_strategy.py @@ -28,13 +28,25 @@ def storage_unit() -> Storage: """ Fixture to create a Storage unit instance with example parameters. """ + # Define the learning configuration for the StorageRLStrategy + learning_config: LearningConfig = { + "observation_dimension": 50, + "action_dimension": 2, + "algorithm": "matd3", + "learning_mode": True, + "training_episodes": 3, + "unit_id": "test_storage", + "max_bid_price": 100, + "max_demand": 1000, + } + index = pd.date_range("2023-06-30 22:00:00", periods=48, freq="h") ff = NaiveForecast(index, availability=1, fuel_price=10, co2_price=10) return Storage( id="test_storage", unit_operator="test_operator", technology="storage", - bidding_strategies={}, + bidding_strategies={"EOM": StorageRLStrategy(**learning_config)}, max_power_charge=500, # Negative for charging max_power_discharge=500, max_soc=1000, @@ -44,7 +56,6 @@ def storage_unit() -> Storage: efficiency_discharge=0.9, additional_cost_charge=5, additional_cost_discharge=5, - index=index, forecaster=ff, ) @@ -65,17 +76,6 @@ def test_storage_rl_strategy_sell_bid(mock_market_config, storage_unit): """ Test the StorageRLStrategy for a 'sell' bid action. """ - # Define the learning configuration for the StorageRLStrategy - learning_config: LearningConfig = { - "observation_dimension": 50, - "action_dimension": 2, - "algorithm": "matd3", - "learning_mode": True, - "training_episodes": 3, - "unit_id": "test_storage", - "max_bid_price": 100, - "max_demand": 1000, - } # Define the product index and tuples product_index = pd.date_range("2023-07-01", periods=1, freq="h") @@ -84,15 +84,17 @@ def test_storage_rl_strategy_sell_bid(mock_market_config, storage_unit): (start, start + pd.Timedelta(hours=1), None) for start in product_index ] - # Instantiate the StorageRLStrategy - strategy = StorageRLStrategy(**learning_config) + # get the strategy + strategy = storage_unit.bidding_strategies["EOM"] # Define the 'sell' action: [0.2, 0.5] -> price=20, direction='sell' sell_action = [0.2, 0.5] # Mock the get_actions method to return the sell action with patch.object( - StorageRLStrategy, "get_actions", return_value=(th.tensor(sell_action), None) + StorageRLStrategy, + "get_actions", + return_value=(th.tensor(sell_action), th.tensor(0.0)), ): # Mock the calculate_marginal_cost method to return a fixed marginal cost with patch.object(Storage, "calculate_marginal_cost", return_value=10.0): @@ -108,9 +110,7 @@ def test_storage_rl_strategy_sell_bid(mock_market_config, storage_unit): bid = bids[0] # Assert the bid price is correctly scaled - expected_bid_price = ( - sell_action[0] * learning_config["max_bid_price"] - ) # 20.0 + expected_bid_price = sell_action[0] * strategy.max_bid_price # 20.0 assert ( bid["price"] == expected_bid_price ), f"Expected bid price {expected_bid_price}, got {bid['price']}" @@ -153,18 +153,18 @@ def test_storage_rl_strategy_sell_bid(mock_market_config, storage_unit): # Assert the calculated reward assert ( - reward.iloc[0] == expected_reward - ), f"Expected reward {expected_reward}, got {reward.iloc[0]}" + reward[0] == expected_reward + ), f"Expected reward {expected_reward}, got {reward[0]}" # Assert the calculated profit assert ( - profit.iloc[0] == expected_profit - expected_costs - ), f"Expected profit {expected_profit}, got {profit.iloc[0]}" + profit[0] == expected_profit - expected_costs + ), f"Expected profit {expected_profit}, got {profit[0]}" # Assert the calculated costs assert ( - costs.iloc[0] == expected_costs - ), f"Expected costs {expected_costs}, got {costs.iloc[0]}" + costs[0] == expected_costs + ), f"Expected costs {expected_costs}, got {costs[0]}" @pytest.mark.require_learning @@ -172,18 +172,6 @@ def test_storage_rl_strategy_buy_bid(mock_market_config, storage_unit): """ Test the StorageRLStrategy for a 'buy' bid action. """ - # Define the learning configuration for the StorageRLStrategy - learning_config: LearningConfig = { - "observation_dimension": 50, - "action_dimension": 2, - "algorithm": "matd3", - "learning_mode": True, - "training_episodes": 3, - "unit_id": "test_storage", - "max_bid_price": 100, - "max_demand": 1000, - } - # Define the product index and tuples product_index = pd.date_range("2023-07-01", periods=1, freq="h") mc = mock_market_config @@ -192,14 +180,16 @@ def test_storage_rl_strategy_buy_bid(mock_market_config, storage_unit): ] # Instantiate the StorageRLStrategy - strategy = StorageRLStrategy(**learning_config) + strategy = storage_unit.bidding_strategies["EOM"] # Define the 'buy' action: [0.3, -0.5] -> price=30, direction='buy' buy_action = [0.3, -0.5] # Mock the get_actions method to return the buy action with patch.object( - StorageRLStrategy, "get_actions", return_value=(th.tensor(buy_action), None) + StorageRLStrategy, + "get_actions", + return_value=(th.tensor(buy_action), th.tensor(0.0)), ): # Mock the calculate_marginal_cost method to return a fixed marginal cost with patch.object(Storage, "calculate_marginal_cost", return_value=15.0): @@ -215,9 +205,7 @@ def test_storage_rl_strategy_buy_bid(mock_market_config, storage_unit): bid = bids[0] # Assert the bid price is correctly scaled - expected_bid_price = ( - buy_action[0] * learning_config["max_bid_price"] - ) # 30.0 + expected_bid_price = buy_action[0] * strategy.max_bid_price # 30.0 assert math.isclose( bid["price"], expected_bid_price, abs_tol=1e3 ), f"Expected bid price {expected_bid_price}, got {bid['price']}" @@ -257,18 +245,18 @@ def test_storage_rl_strategy_buy_bid(mock_market_config, storage_unit): # Assert the calculated reward assert ( - reward.iloc[0] == expected_reward - ), f"Expected reward {expected_reward}, got {reward.iloc[0]}" + reward[0] == expected_reward + ), f"Expected reward {expected_reward}, got {reward[0]}" # Assert the calculated profit assert ( - profit.iloc[0] == expected_profit - expected_costs - ), f"Expected profit {expected_profit}, got {profit.iloc[0]}" + profit[0] == expected_profit - expected_costs + ), f"Expected profit {expected_profit}, got {profit[0]}" # Assert the calculated costs assert ( - costs.iloc[0] == expected_costs - ), f"Expected costs {expected_costs}, got {costs.iloc[0]}" + costs[0] == expected_costs + ), f"Expected costs {expected_costs}, got {costs[0]}" @pytest.mark.require_learning @@ -276,17 +264,6 @@ def test_storage_rl_strategy_ignore_bid(mock_market_config, storage_unit): """ Test the StorageRLStrategy for an 'ignore' bid action. """ - # Define the learning configuration for the StorageRLStrategy - learning_config: LearningConfig = { - "observation_dimension": 50, - "action_dimension": 2, - "algorithm": "matd3", - "learning_mode": True, - "training_episodes": 3, - "unit_id": "test_storage", - "max_bid_price": 100, - "max_demand": 1000, - } # Define the product index and tuples product_index = pd.date_range("2023-07-01", periods=1, freq="h") @@ -296,14 +273,16 @@ def test_storage_rl_strategy_ignore_bid(mock_market_config, storage_unit): ] # Instantiate the StorageRLStrategy - strategy = StorageRLStrategy(**learning_config) + strategy = storage_unit.bidding_strategies["EOM"] # Define the 'ignore' action: [0.0, 0.0] -> price=0, direction='ignore' ignore_action = [0.0, 0.0] # Mock the get_actions method to return the ignore action with patch.object( - StorageRLStrategy, "get_actions", return_value=(th.tensor(ignore_action), None) + StorageRLStrategy, + "get_actions", + return_value=(th.tensor(ignore_action), th.tensor(0.0)), ): # Mock the calculate_marginal_cost method to return a fixed marginal cost with patch.object(Storage, "calculate_marginal_cost", return_value=0.0): @@ -319,9 +298,7 @@ def test_storage_rl_strategy_ignore_bid(mock_market_config, storage_unit): bid = bids[0] # Assert the bid price is correctly scaled - expected_bid_price = ( - ignore_action[0] * learning_config["max_bid_price"] - ) # 0.0 + expected_bid_price = ignore_action[0] * strategy.max_bid_price # 0.0 assert ( bid["price"] == expected_bid_price ), f"Expected bid price {expected_bid_price}, got {bid['price']}" @@ -355,15 +332,15 @@ def test_storage_rl_strategy_ignore_bid(mock_market_config, storage_unit): # Assert the calculated reward assert ( - reward.iloc[0] == expected_reward - ), f"Expected reward {expected_reward}, got {reward.iloc[0]}" + reward[0] == expected_reward + ), f"Expected reward {expected_reward}, got {reward[0]}" # Assert the calculated profit assert ( - profit.iloc[0] == expected_profit - ), f"Expected profit {expected_profit}, got {profit.iloc[0]}" + profit[0] == expected_profit + ), f"Expected profit {expected_profit}, got {profit[0]}" # Assert the calculated costs assert ( - costs.iloc[0] == expected_costs - ), f"Expected costs {expected_costs}, got {costs.iloc[0]}" + costs[0] == expected_costs + ), f"Expected costs {expected_costs}, got {costs[0]}" diff --git a/tests/test_flexable_storage_strategies.py b/tests/test_flexable_storage_strategies.py index 160692206..85bb98f7e 100644 --- a/tests/test_flexable_storage_strategies.py +++ b/tests/test_flexable_storage_strategies.py @@ -22,18 +22,19 @@ def storage() -> Storage: # Create a PowerPlant instance with some example parameters index = pd.date_range("2023-07-01", periods=48, freq="h") + forecaster = NaiveForecast(index, availability=1, price_forecast=50) return Storage( id="Test_Storage", unit_operator="TestOperator", technology="TestTechnology", bidding_strategies={}, + forecaster=forecaster, max_power_charge=-100, max_power_discharge=100, max_soc=1000, initial_soc=500, efficiency_charge=0.9, efficiency_discharge=0.95, - index=index, ramp_down_charge=-50, ramp_down_discharge=50, ramp_up_charge=-60, diff --git a/tests/test_flexable_strategies.py b/tests/test_flexable_strategies.py index ee9189867..cc17c5c2a 100644 --- a/tests/test_flexable_strategies.py +++ b/tests/test_flexable_strategies.py @@ -24,12 +24,12 @@ def power_plant() -> PowerPlant: # Create a PowerPlant instance with some example parameters index = pd.date_range("2023-07-01", periods=48, freq="h") - ff = NaiveForecast(index, availability=1, fuel_price=10, co2_price=10) + forecaster = NaiveForecast(index, availability=1, fuel_price=10, co2_price=10) return PowerPlant( id="test_pp", unit_operator="test_operator", technology="hard coal", - index=index, + index=forecaster.index, max_power=1000, min_power=200, efficiency=0.5, @@ -37,7 +37,7 @@ def power_plant() -> PowerPlant: bidding_strategies={}, fuel_type="lignite", emission_factor=0.5, - forecaster=ff, + forecaster=forecaster, ) diff --git a/tests/test_naive_strategies.py b/tests/test_naive_strategies.py index ce743d139..18f68a6c8 100644 --- a/tests/test_naive_strategies.py +++ b/tests/test_naive_strategies.py @@ -4,7 +4,6 @@ from datetime import datetime -import pandas as pd from dateutil.relativedelta import relativedelta as rd from assume.common.market_objects import MarketProduct @@ -15,7 +14,6 @@ NaiveProfileStrategy, NaiveSingleBidStrategy, ) -from tests.conftest import MockMinMaxUnit start = datetime(2023, 7, 1) end = datetime(2023, 7, 2) @@ -73,12 +71,6 @@ def test_naive_da_strategy(mock_market_config, mock_supports_minmax): next_opening = start products = get_available_products(mc.market_products, next_opening) assert len(products) == 24 - index = pd.date_range( - start=products[0][0], - end=products[-1][0], - freq="1h", - ) - unit = MockMinMaxUnit(index) bids = strategy.calculate_bids(unit, mc, product_tuples=products) assert bids[0]["price"] == 3 diff --git a/tests/test_outputs.py b/tests/test_outputs.py index c22a03ddd..6a64902a1 100644 --- a/tests/test_outputs.py +++ b/tests/test_outputs.py @@ -5,6 +5,7 @@ import os from datetime import datetime +import numpy as np import pandas as pd from sqlalchemy import create_engine @@ -139,19 +140,19 @@ def test_output_unit_dispatch(): content = { "context": "write_results", "type": "unit_dispatch", - "data": pd.DataFrame( + "data": [ { - "power": { - pd.Timestamp("2019-01-01 00:00:00"): 0.0, - pd.Timestamp("2019-01-01 01:00:00"): 0.0, - }, - "unit": { - pd.Timestamp("2019-01-01 00:00:00"): "demand_EOM", - pd.Timestamp("2019-01-01 01:00:00"): "demand_EOM", - }, + "power": np.array([0.0, 1000.0]), + "energy_cashflow": np.array([0.0, 45050.0]), + "time": [ + pd.Timestamp("2019-01-01 00:00:00"), + pd.Timestamp("2019-01-01 01:00:00"), + ], + "unit": "Unit 2", } - ), + ], } + output_writer.handle_output_message(content, meta) assert len(output_writer.write_dfs["unit_dispatch"]) == 1, "unit_dispatch" diff --git a/tests/test_policies.py b/tests/test_policies.py index 032dfc2a6..62c3bd52d 100644 --- a/tests/test_policies.py +++ b/tests/test_policies.py @@ -18,6 +18,7 @@ ) from mango.util.clock import ExternalClock +from assume.common.fast_pandas import FastIndex from assume.common.forecasts import NaiveForecast from assume.common.market_objects import MarketConfig, MarketProduct from assume.common.units_operator import UnitsOperator @@ -74,7 +75,7 @@ async def test_request_messages(): units_agent.add_role(units_role) container.register(units_agent, suggested_aid="test_operator") - index = pd.date_range(start=start, end=end + pd.Timedelta(hours=4), freq="1h") + index = FastIndex(start=start, end=end + pd.Timedelta(hours=4), freq="1h") params_dict = { "bidding_strategies": {"energy": NaiveSingleBidStrategy()}, @@ -84,7 +85,7 @@ async def test_request_messages(): "min_power": 0, "forecaster": NaiveForecast(index, demand=1000), } - unit = Demand("testdemand", index=index, **params_dict) + unit = Demand("testdemand", **params_dict) units_role.add_unit(unit) market_role = MarketRole(marketconfig) diff --git a/tests/test_powerplant.py b/tests/test_powerplant.py index ae21cd123..cf3fb27df 100644 --- a/tests/test_powerplant.py +++ b/tests/test_powerplant.py @@ -16,7 +16,7 @@ def power_plant_1() -> PowerPlant: # Create a PowerPlant instance with some example parameters index = pd.date_range("2022-01-01", periods=4, freq="h") - ff = NaiveForecast( + forecaster = NaiveForecast( index, availability=1, fuel_price=[10, 11, 12, 13], co2_price=[10, 20, 30, 30] ) return PowerPlant( @@ -24,14 +24,14 @@ def power_plant_1() -> PowerPlant: unit_operator="test_operator", technology="hard coal", bidding_strategies={"EOM": NaiveSingleBidStrategy()}, - index=index, + index=forecaster.index, max_power=1000, min_power=200, efficiency=0.5, additional_cost=10, fuel_type="lignite", emission_factor=0.5, - forecaster=ff, + forecaster=forecaster, ) @@ -39,19 +39,19 @@ def power_plant_1() -> PowerPlant: def power_plant_2() -> PowerPlant: # Create a PowerPlant instance with some example parameters index = pd.date_range("2022-01-01", periods=4, freq="h") - ff = NaiveForecast(index, availability=1, fuel_price=10, co2_price=10) + forecaster = NaiveForecast(index, availability=1, fuel_price=10, co2_price=10) return PowerPlant( id="test_pp", unit_operator="test_operator", technology="hard coal", bidding_strategies={"EOM": NaiveSingleBidStrategy()}, - index=index, + index=forecaster.index, max_power=1000, min_power=0, efficiency=0.5, additional_cost=10, fuel_type="lignite", - forecaster=ff, + forecaster=forecaster, emission_factor=0.5, ) @@ -60,20 +60,20 @@ def power_plant_2() -> PowerPlant: def power_plant_3() -> PowerPlant: # Create a PowerPlant instance with some example parameters index = pd.date_range("2022-01-01", periods=4, freq="h") - ff = NaiveForecast(index, availability=1, fuel_price=10, co2_price=10) + forecaster = NaiveForecast(index, availability=1, fuel_price=10, co2_price=10) return PowerPlant( id="test_pp", unit_operator="test_operator", technology="hard coal", bidding_strategies={"EOM": NaiveSingleBidStrategy()}, - index=index, + index=forecaster.index, max_power=1000, min_power=0, efficiency=0.5, additional_cost=10, fuel_type="lignite", emission_factor=0.5, - forecaster=ff, + forecaster=forecaster, partial_load_eff=True, ) @@ -90,39 +90,32 @@ def test_init_function(power_plant_1, power_plant_2, power_plant_3): assert power_plant_1.emission_factor == 0.5 assert power_plant_1.ramp_up == 1000 assert power_plant_1.ramp_down == 1000 + index = pd.date_range("2022-01-01", periods=4, freq="h") assert ( - power_plant_1.marginal_cost.to_dict() - == pd.Series( - [40.0, 52.0, 64.0, 66.0], - index, - ).to_dict() - ) + power_plant_1.marginal_cost == pd.Series([40.0, 52.0, 64.0, 66.0], index) + ).all() - assert power_plant_2.marginal_cost.to_dict() == pd.Series(40, index).to_dict() - assert power_plant_3.marginal_cost.to_dict() == pd.Series(40, index).to_dict() + assert (power_plant_2.marginal_cost == pd.Series(40, index)).all() + assert (power_plant_3.marginal_cost == pd.Series(40, index)).all() def test_reset_function(power_plant_1): - # check if total_power_output is reset - assert power_plant_1.outputs["energy"].equals( - pd.Series(0.0, index=pd.date_range("2022-01-01", periods=4, freq="h")) - ) - # the same for pos and neg capacity reserve - assert power_plant_1.outputs["pos_capacity"].equals( - pd.Series(0.0, index=pd.date_range("2022-01-01", periods=4, freq="h")) - ) - assert power_plant_1.outputs["neg_capacity"].equals( - pd.Series(0.0, index=pd.date_range("2022-01-01", periods=4, freq="h")) + # Expected series with zero values + expected_series = pd.Series( + 0.0, index=pd.date_range("2022-01-01", periods=4, freq="h") ) - # the same for total_heat_output and power_loss_chp - assert power_plant_1.outputs["heat"].equals( - pd.Series(0.0, index=pd.date_range("2022-01-01", periods=4, freq="h")) - ) - assert power_plant_1.outputs["power_loss"].equals( - pd.Series(0.0, index=pd.date_range("2022-01-01", periods=4, freq="h")) - ) + # Check if total_power_output is reset + assert (power_plant_1.outputs["energy"].data == expected_series.values).all() + + # The same for pos and neg capacity reserve + assert (power_plant_1.outputs["pos_capacity"].data == expected_series.values).all() + assert (power_plant_1.outputs["neg_capacity"].data == expected_series.values).all() + + # The same for total_heat_output and power_loss_chp + assert (power_plant_1.outputs["heat"].data == expected_series.values).all() + assert (power_plant_1.outputs["power_loss"].data == expected_series.values).all() def test_calculate_operational_window(power_plant_1): @@ -137,13 +130,13 @@ def test_calculate_operational_window(power_plant_1): start, end, product_type="energy" ) - min_cost = power_plant_1.calculate_marginal_cost(start, min_power[start]) - max_cost = power_plant_1.calculate_marginal_cost(start, max_power[start]) + min_cost = power_plant_1.calculate_marginal_cost(start, min_power[0]) + max_cost = power_plant_1.calculate_marginal_cost(start, max_power[0]) - assert min_power[start] == 200 + assert min_power[0] == 200 assert min_cost == 40.0 - assert max_power[start] == 1000 + assert max_power[0] == 1000 assert max_cost == 40 assert power_plant_1.outputs["energy"].at[start] == 0 @@ -155,19 +148,18 @@ def test_powerplant_feedback(power_plant_1, mock_market_config): pd.Timestamp("2022-01-01 01:00:00"), None, ) - product_type = "energy" start = product_tuple[0] end = product_tuple[1] min_power, max_power = power_plant_1.calculate_min_max_power( start, end, product_type="energy" ) - min_cost = power_plant_1.calculate_marginal_cost(start, min_power[start]) - max_cost = power_plant_1.calculate_marginal_cost(start, max_power[start]) + min_cost = power_plant_1.calculate_marginal_cost(start, min_power[0]) + max_cost = power_plant_1.calculate_marginal_cost(start, max_power[0]) - assert min_power[start] == 200 + assert min_power[0] == 200 assert min_cost == 40.0 - assert max_power[start] == 1000 + assert max_power[0] == 1000 assert max_cost == 40 assert power_plant_1.outputs["energy"].at[start] == 0 @@ -178,7 +170,7 @@ def test_powerplant_feedback(power_plant_1, mock_market_config): "only_hours": None, "price": min_cost, "accepted_price": min_cost, - "accepted_volume": min_power[start], + "accepted_volume": min_power[0], } ] @@ -192,9 +184,9 @@ def test_powerplant_feedback(power_plant_1, mock_market_config): ) # we do not need additional min_power, as our runtime requirement is fulfilled - assert min_power[start] == 0 + assert min_power[0] == 0 # we can not bid the maximum anymore, because we already provide energy on the other market - assert max_power[start] == 800 + assert max_power[0] == 800 # second market request for next interval start = pd.Timestamp("2022-01-01 01:00:00") @@ -204,8 +196,8 @@ def test_powerplant_feedback(power_plant_1, mock_market_config): ) # now we can bid max_power and need min_power again - assert min_power[start] == 200 - assert max_power[start] == 1000 + assert min_power[0] == 200 + assert max_power[0] == 1000 def test_powerplant_ramping(power_plant_1): @@ -222,16 +214,16 @@ def test_powerplant_ramping(power_plant_1): start, end, product_type="energy" ) - assert min_power[start] == 50 - assert max_power[start] == 1000 + assert min_power[0] == 50 + assert max_power[0] == 1000 op_time = power_plant_1.get_operation_time(start) assert op_time == 3 - min_cost = power_plant_1.calculate_marginal_cost(start, min_power[start]) - max_cost = power_plant_1.calculate_marginal_cost(start, max_power[start]) - max_ramp = power_plant_1.calculate_ramp(op_time, 100, max_power[start]) - min_ramp = power_plant_1.calculate_ramp(op_time, 100, min_power[start]) + min_cost = power_plant_1.calculate_marginal_cost(start, min_power[0]) + max_cost = power_plant_1.calculate_marginal_cost(start, max_power[0]) + max_ramp = power_plant_1.calculate_ramp(op_time, 100, max_power[0]) + min_ramp = power_plant_1.calculate_ramp(op_time, 100, min_power[0]) assert min_ramp == 50 assert min_cost == 40.0 @@ -252,14 +244,14 @@ def test_powerplant_ramping(power_plant_1): start, end, product_type="energy" ) - assert min_power[start] == 50 - assert max_power[start] == 1000 + assert min_power[0] == 50 + assert max_power[0] == 1000 op_time = power_plant_1.get_operation_time(start) assert op_time == 1 - min_ramp = power_plant_1.calculate_ramp(op_time, 300, min_power[start]) - max_ramp = power_plant_1.calculate_ramp(op_time, 300, max_power[start]) + min_ramp = power_plant_1.calculate_ramp(op_time, 300, min_power[0]) + max_ramp = power_plant_1.calculate_ramp(op_time, 300, max_power[0]) assert min_ramp == 200 assert max_ramp == 500 @@ -279,8 +271,8 @@ def test_powerplant_ramping(power_plant_1): op_time = power_plant_1.get_operation_time(start) assert op_time == 2 - min_ramp = power_plant_1.calculate_ramp(op_time, 500, min_power[start]) - max_ramp = power_plant_1.calculate_ramp(op_time, 500, max_power[start]) + min_ramp = power_plant_1.calculate_ramp(op_time, 500, min_power[0]) + max_ramp = power_plant_1.calculate_ramp(op_time, 500, max_power[0]) assert min_ramp == 400 assert max_ramp == 700 @@ -333,7 +325,7 @@ def test_powerplant_availability(power_plant_1): start, end, product_type="energy" ) op_time = power_plant_1.get_operation_time(start) - max_ramp = power_plant_1.calculate_ramp(op_time, 0, max_power[start]) + max_ramp = power_plant_1.calculate_ramp(op_time, 0, max_power[0]) assert max_ramp == power_plant_1.max_power / 2 ### HOUR 1 @@ -344,7 +336,7 @@ def test_powerplant_availability(power_plant_1): ) op_time = power_plant_1.get_operation_time(start) # run min_power if 0 < power <= min_power is needed - max_ramp = power_plant_1.calculate_ramp(op_time, 0, max_power[start]) + max_ramp = power_plant_1.calculate_ramp(op_time, 0, max_power[0]) assert max_ramp == power_plant_1.min_power ### HOUR 2 @@ -354,19 +346,19 @@ def test_powerplant_availability(power_plant_1): start, end, product_type="energy" ) op_time = power_plant_1.get_operation_time(start) - max_ramp = power_plant_1.calculate_ramp(op_time, 0, max_power[start]) + max_ramp = power_plant_1.calculate_ramp(op_time, 0, max_power[0]) assert max_ramp == power_plant_1.max_power def test_powerplant_execute_dispatch(): index = pd.date_range("2022-01-01", periods=24, freq="h") - ff = NaiveForecast(index, availability=1, fuel_price=10, co2_price=10) + forecaster = NaiveForecast(index, availability=1, fuel_price=10, co2_price=10) power_plant = PowerPlant( id="test_pp", unit_operator="test_operator", technology="coal", bidding_strategies={"EOM": NaiveSingleBidStrategy()}, - index=index, + index=forecaster.index, max_power=700, min_power=50, efficiency=0.5, @@ -375,10 +367,10 @@ def test_powerplant_execute_dispatch(): ramp_up=200, min_operating_time=3, min_down_time=2, - forecaster=ff, + forecaster=forecaster, ) # was running before - assert power_plant.execute_current_dispatch(index[0], index[0]).iloc[0] == 0 + assert power_plant.execute_current_dispatch(index[0], index[0])[0] == 0 power_plant.outputs["energy"].loc[index] = [ 0, diff --git a/tests/test_rl_advanced_order_strategy.py b/tests/test_rl_advanced_order_strategy.py index 6f500b651..a16d5a9d9 100644 --- a/tests/test_rl_advanced_order_strategy.py +++ b/tests/test_rl_advanced_order_strategy.py @@ -25,16 +25,24 @@ def power_plant() -> PowerPlant: # Create a PowerPlant instance with some example parameters index = pd.date_range("2023-07-01", periods=48, freq="h") ff = NaiveForecast(index, availability=1, fuel_price=10, co2_price=10) + learning_config: LearningConfig = { + "algorithm": "matd3", + "learning_mode": True, + "training_episodes": 3, + "order_types": ["SB", "BB", "LB"], + "unit_id": "test_pp", + } + return PowerPlant( id="test_pp", unit_operator="test_operator", technology="coal", - index=index, + index=ff.index, max_power=1000, min_power=200, efficiency=0.5, additional_cost=10, - bidding_strategies={}, + bidding_strategies={"EOM": RLAdvancedOrderStrategy(**learning_config)}, fuel_type="lignite", emission_factor=0.5, forecaster=ff, @@ -43,14 +51,6 @@ def power_plant() -> PowerPlant: @pytest.mark.require_learning def test_learning_advanced_orders(mock_market_config, power_plant): - learning_config: LearningConfig = { - "algorithm": "matd3", - "learning_mode": True, - "training_episodes": 3, - "order_types": ["SB", "BB", "LB"], - "unit_id": "test_pp", - } - product_index = pd.date_range("2023-07-01", periods=24, freq="h") mc = mock_market_config mc.product_type = "energy_eom" @@ -58,8 +58,9 @@ def test_learning_advanced_orders(mock_market_config, power_plant): (start, start + pd.Timedelta(hours=1), None) for start in product_index ] - learning_config["order_types"] = ["SB"] - strategy = RLAdvancedOrderStrategy(**learning_config) + strategy = power_plant.bidding_strategies["EOM"] + + strategy.order_types = ["SB"] bids = strategy.calculate_bids(power_plant, mc, product_tuples=product_tuples) assert len(bids) == 48 @@ -68,8 +69,7 @@ def test_learning_advanced_orders(mock_market_config, power_plant): assert bids[0]["bid_id"] == "test_pp_SB_1" assert bids[-1]["bid_id"] == "test_pp_SB_48" - learning_config["order_types"] = ["SB", "BB"] - strategy = RLAdvancedOrderStrategy(**learning_config) + strategy.order_types = ["SB", "BB"] bids = strategy.calculate_bids(power_plant, mc, product_tuples=product_tuples) assert len(bids) == 25 @@ -82,8 +82,7 @@ def test_learning_advanced_orders(mock_market_config, power_plant): assert bids[-1]["volume"][product_tuples[0][0]] == 200 assert bids[0]["price"] >= bids[-1]["price"] - learning_config["order_types"] = ["SB", "LB"] - strategy = RLAdvancedOrderStrategy(**learning_config) + strategy.order_types = ["SB", "LB"] bids = strategy.calculate_bids(power_plant, mc, product_tuples=product_tuples) assert len(bids) == 48 @@ -97,8 +96,7 @@ def test_learning_advanced_orders(mock_market_config, power_plant): assert bids[0]["bid_id"] == "test_pp_SB_1" assert bids[-1]["bid_id"] == "test_pp_LB_48" - learning_config["order_types"] = ["SB", "BB", "LB"] - strategy = RLAdvancedOrderStrategy(**learning_config) + strategy.order_types = ["SB", "BB", "LB"] bids = strategy.calculate_bids(power_plant, mc, product_tuples=product_tuples) assert len(bids) == 25 diff --git a/tests/test_rl_strategies.py b/tests/test_rl_strategies.py index e514751ba..c91da41b0 100644 --- a/tests/test_rl_strategies.py +++ b/tests/test_rl_strategies.py @@ -21,37 +21,68 @@ @pytest.fixture -def power_plant() -> PowerPlant: +def power_plant_mcp() -> PowerPlant: # Create a PowerPlant instance with some example parameters index = pd.date_range("2023-06-30 22:00:00", periods=48, freq="h") ff = NaiveForecast(index, availability=1, fuel_price=10, co2_price=10) + learning_config: LearningConfig = { + "observation_dimension": 50, + "action_dimension": 2, + "algorithm": "matd3", + "learning_mode": True, + "training_episodes": 3, + "unit_id": "test_pp", + } + return PowerPlant( id="test_pp", unit_operator="test_operator", technology="coal", - index=index, + index=ff.index, max_power=1000, min_power=200, efficiency=0.5, additional_cost=10, - bidding_strategies={}, + bidding_strategies={"EOM": RLStrategy(**learning_config)}, fuel_type="lignite", emission_factor=0.5, forecaster=ff, ) -@pytest.mark.require_learning -def test_learning_strategies(mock_market_config, power_plant): +@pytest.fixture +def power_plant_lstm() -> PowerPlant: + # Create a PowerPlant instance with some example parameters + index = pd.date_range("2023-06-30 22:00:00", periods=48, freq="h") + ff = NaiveForecast(index, availability=1, fuel_price=10, co2_price=10) learning_config: LearningConfig = { "observation_dimension": 50, "action_dimension": 2, "algorithm": "matd3", + "actor_architecture": "lstm", "learning_mode": True, "training_episodes": 3, "unit_id": "test_pp", } + return PowerPlant( + id="test_pp", + unit_operator="test_operator", + technology="coal", + index=ff.index, + max_power=1000, + min_power=200, + efficiency=0.5, + additional_cost=10, + bidding_strategies={"EOM": RLStrategy(**learning_config)}, + fuel_type="lignite", + emission_factor=0.5, + forecaster=ff, + ) + + +@pytest.mark.require_learning +def test_learning_strategies(mock_market_config, power_plant_mcp): product_index = pd.date_range("2023-07-01", periods=1, freq="h") mc = mock_market_config mc.product_type = "energy_eom" @@ -59,8 +90,8 @@ def test_learning_strategies(mock_market_config, power_plant): (start, start + pd.Timedelta(hours=1), None) for start in product_index ] - strategy = RLStrategy(**learning_config) - bids = strategy.calculate_bids(power_plant, mc, product_tuples=product_tuples) + strategy = power_plant_mcp.bidding_strategies["EOM"] + bids = strategy.calculate_bids(power_plant_mcp, mc, product_tuples=product_tuples) assert len(bids) == 2 assert bids[0]["volume"] == 200 @@ -70,30 +101,20 @@ def test_learning_strategies(mock_market_config, power_plant): order["accepted_price"] = 50 order["accepted_volume"] = order["volume"] - strategy.calculate_reward(power_plant, mc, orderbook=bids) - reward = power_plant.outputs["reward"].loc[product_index] - profit = power_plant.outputs["profit"].loc[product_index] - regret = power_plant.outputs["regret"].loc[product_index] - costs = power_plant.outputs["total_costs"].loc[product_index] + strategy.calculate_reward(power_plant_mcp, mc, orderbook=bids) + reward = power_plant_mcp.outputs["reward"].loc[product_index] + profit = power_plant_mcp.outputs["profit"].loc[product_index] + regret = power_plant_mcp.outputs["regret"].loc[product_index] + costs = power_plant_mcp.outputs["total_costs"].loc[product_index] - assert reward.iloc[0] == 0.8 - assert profit.iloc[0] == 10000.0 - assert regret.iloc[0] == 2000.0 - assert costs.iloc[0] == 40000.0 + assert reward[0] == 0.8 + assert profit[0] == 10000.0 + assert regret[0] == 2000.0 + assert costs[0] == 40000.0 @pytest.mark.require_learning -def test_lstm_learning_strategies(mock_market_config, power_plant): - learning_config: LearningConfig = { - "observation_dimension": 50, - "action_dimension": 2, - "algorithm": "matd3", - "actor_architecture": "lstm", - "learning_mode": True, - "training_episodes": 3, - "unit_id": "test_pp", - } - +def test_lstm_learning_strategies(mock_market_config, power_plant_lstm): product_index = pd.date_range("2023-07-01", periods=1, freq="h") mc = mock_market_config mc.product_type = "energy_eom" @@ -101,8 +122,8 @@ def test_lstm_learning_strategies(mock_market_config, power_plant): (start, start + pd.Timedelta(hours=1), None) for start in product_index ] - strategy = RLStrategy(**learning_config) - bids = strategy.calculate_bids(power_plant, mc, product_tuples=product_tuples) + strategy = power_plant_lstm.bidding_strategies["EOM"] + bids = strategy.calculate_bids(power_plant_lstm, mc, product_tuples=product_tuples) assert len(bids) == 2 assert bids[0]["volume"] == 200 @@ -112,13 +133,13 @@ def test_lstm_learning_strategies(mock_market_config, power_plant): order["accepted_price"] = 50 order["accepted_volume"] = order["volume"] - strategy.calculate_reward(power_plant, mc, orderbook=bids) - reward = power_plant.outputs["reward"].loc[product_index] - profit = power_plant.outputs["profit"].loc[product_index] - regret = power_plant.outputs["regret"].loc[product_index] - costs = power_plant.outputs["total_costs"].loc[product_index] + strategy.calculate_reward(power_plant_lstm, mc, orderbook=bids) + reward = power_plant_lstm.outputs["reward"].loc[product_index] + profit = power_plant_lstm.outputs["profit"].loc[product_index] + regret = power_plant_lstm.outputs["regret"].loc[product_index] + costs = power_plant_lstm.outputs["total_costs"].loc[product_index] - assert reward.iloc[0] == 0.8 - assert profit.iloc[0] == 10000.0 - assert regret.iloc[0] == 2000.0 - assert costs.iloc[0] == 40000.0 + assert reward[0] == 0.8 + assert profit[0] == 10000.0 + assert regret[0] == 2000.0 + assert costs[0] == 40000.0 diff --git a/tests/test_steel_plant.py b/tests/test_steel_plant.py index dd14e20ed..6c8bbecf6 100644 --- a/tests/test_steel_plant.py +++ b/tests/test_steel_plant.py @@ -5,6 +5,7 @@ import pandas as pd import pytest +from assume.common.fast_pandas import FastSeries from assume.common.forecasts import NaiveForecast from assume.strategies.naive_strategies import ( NaiveDASteelplantStrategy, @@ -73,7 +74,7 @@ def steel_plant(dsm_components) -> SteelPlant: objective="min_variable_cost", flexibility_measure="max_load_shift", bidding_strategies=bidding_strategies, - index=index, + index=forecast.index, components=dsm_components, forecaster=forecast, demand=1000, @@ -89,7 +90,7 @@ def test_initialize_components(steel_plant): def test_determine_optimal_operation_without_flex(steel_plant): steel_plant.determine_optimal_operation_without_flex() assert steel_plant.opt_power_requirement is not None - assert isinstance(steel_plant.opt_power_requirement, pd.Series) + assert isinstance(steel_plant.opt_power_requirement, FastSeries) instance = steel_plant.model.create_instance() instance = steel_plant.switch_to_opt(instance) @@ -134,11 +135,11 @@ def test_determine_optimal_operation_with_flex(steel_plant): # Ensure that the optimal operation without flexibility is determined first steel_plant.determine_optimal_operation_without_flex() assert steel_plant.opt_power_requirement is not None - assert isinstance(steel_plant.opt_power_requirement, pd.Series) + assert isinstance(steel_plant.opt_power_requirement, FastSeries) steel_plant.determine_optimal_operation_with_flex() assert steel_plant.flex_power_requirement is not None - assert isinstance(steel_plant.flex_power_requirement, pd.Series) + assert isinstance(steel_plant.flex_power_requirement, FastSeries) instance = steel_plant.model.create_instance() instance = steel_plant.switch_to_flex(instance) diff --git a/tests/test_storage.py b/tests/test_storage.py index 91b86e684..9e930b472 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -8,6 +8,7 @@ import pandas as pd import pytest +from assume.common.forecasts import NaiveForecast from assume.strategies.flexable_storage import flexableEOMStorage from assume.strategies.naive_strategies import NaiveSingleBidStrategy from assume.units import Storage @@ -15,17 +16,19 @@ @pytest.fixture def storage_unit() -> Storage: + index = pd.date_range("2022-01-01", periods=4, freq="h") + forecaster = NaiveForecast(index, availability=1, price_forecast=50) return Storage( id="Test_Storage", unit_operator="TestOperator", technology="TestTechnology", bidding_strategies={"EOM": NaiveSingleBidStrategy()}, + forecaster=forecaster, max_power_charge=-100, max_power_discharge=100, max_soc=1000, efficiency_charge=0.9, efficiency_discharge=0.95, - index=pd.date_range("2022-01-01", periods=4, freq="h"), ramp_down_charge=-50, ramp_down_discharge=50, ramp_up_charge=-60, @@ -54,19 +57,26 @@ def test_init_function(storage_unit): def test_reset_function(storage_unit): # check if total_power_output is reset - assert storage_unit.outputs["energy"].equals( - pd.Series(0.0, index=pd.date_range("2022-01-01", periods=4, freq="h")) - ) + assert ( + storage_unit.outputs["energy"] + == pd.Series(0.0, index=pd.date_range("2022-01-01", periods=4, freq="h")) + ).all() + # the same for pos and neg capacity reserve - assert storage_unit.outputs["pos_capacity"].equals( - pd.Series(0.0, index=pd.date_range("2022-01-01", periods=4, freq="h")) - ) - assert storage_unit.outputs["neg_capacity"].equals( - pd.Series(0.0, index=pd.date_range("2022-01-01", periods=4, freq="h")) - ) - assert storage_unit.outputs["soc"].equals( - pd.Series(500.0, index=pd.date_range("2022-01-01", periods=4, freq="h")) - ) + assert ( + storage_unit.outputs["pos_capacity"] + == pd.Series(0.0, index=pd.date_range("2022-01-01", periods=4, freq="h")) + ).all() + assert ( + storage_unit.outputs["neg_capacity"] + == pd.Series(0.0, index=pd.date_range("2022-01-01", periods=4, freq="h")) + ).all() + + # check if state of charge (soc) is reset correctly + assert ( + storage_unit.outputs["soc"] + == pd.Series(500.0, index=pd.date_range("2022-01-01", periods=4, freq="h")) + ).all() def test_calculate_operational_window(storage_unit): @@ -80,21 +90,19 @@ def test_calculate_operational_window(storage_unit): min_power_discharge, max_power_discharge = storage_unit.calculate_min_max_discharge( start, end, product_type="energy" ) - cost_discharge = storage_unit.calculate_marginal_cost( - start, max_power_discharge[start] - ) + cost_discharge = storage_unit.calculate_marginal_cost(start, max_power_discharge[0]) - assert min_power_discharge[start] == 0 - assert max_power_discharge[start] == 100 + assert min_power_discharge[0] == 0 + assert max_power_discharge[0] == 100 assert cost_discharge == 4 / 0.95 min_power_charge, max_power_charge = storage_unit.calculate_min_max_charge( start, end, product_type="energy" ) - cost_charge = storage_unit.calculate_marginal_cost(start, max_power_charge[start]) + cost_charge = storage_unit.calculate_marginal_cost(start, max_power_charge[0]) - assert min_power_charge[start] == 0 - assert max_power_charge[start] == -100 + assert min_power_charge[0] == 0 + assert max_power_charge[0] == -100 assert math.isclose(cost_charge, 3 / 0.9) assert storage_unit.outputs["energy"].at[start] == 0 @@ -106,14 +114,14 @@ def test_calculate_operational_window(storage_unit): min_power_charge, max_power_charge = storage_unit.calculate_min_max_charge( start, end ) - assert min_power_charge.iloc[0] == -40 - assert max_power_charge.iloc[0] == -60 + assert min_power_charge[0] == -40 + assert max_power_charge[0] == -60 min_power_discharge, max_power_discharge = storage_unit.calculate_min_max_discharge( start, end ) - assert min_power_discharge.iloc[0] == 40 - assert max_power_discharge.iloc[0] == 60 + assert min_power_discharge[0] == 40 + assert max_power_discharge[0] == 60 start = start + timedelta(hours=1) @@ -136,18 +144,18 @@ def test_soc_constraint(storage_unit): min_power_discharge, max_power_discharge = storage_unit.calculate_min_max_discharge( start, end ) - assert min_power_discharge.iloc[0] == 40 + assert min_power_discharge[0] == 40 assert math.isclose( - max_power_discharge.iloc[0], (50 * storage_unit.efficiency_discharge) + max_power_discharge[0], (50 * storage_unit.efficiency_discharge) ) storage_unit.outputs["soc"][start] = 0.95 * storage_unit.max_soc min_power_charge, max_power_charge = storage_unit.calculate_min_max_charge( start, end ) - assert min_power_charge.iloc[0] == -40 + assert min_power_charge[0] == -40 assert math.isclose( - max_power_charge.iloc[0], -50 / storage_unit.efficiency_charge, abs_tol=0.1 + max_power_charge[0], -50 / storage_unit.efficiency_charge, abs_tol=0.1 ) @@ -157,27 +165,23 @@ def test_storage_feedback(storage_unit, mock_market_config): pd.Timestamp("2022-01-01 01:00:00"), None, ) - product_type = "energy" start = product_tuple[0] end = product_tuple[1] min_power_charge, max_power_charge = storage_unit.calculate_min_max_charge( start, end, product_type="energy" ) - cost_charge = storage_unit.calculate_marginal_cost(start, max_power_charge[start]) min_power_discharge, max_power_discharge = storage_unit.calculate_min_max_discharge( start, end, product_type="energy" ) - cost_discharge = storage_unit.calculate_marginal_cost( - start, max_power_discharge[start] - ) + cost_discharge = storage_unit.calculate_marginal_cost(start, max_power_discharge[0]) - assert min_power_charge[start] == 0 - assert max_power_charge[start] == -100 + assert min_power_charge[0] == 0 + assert max_power_charge[0] == -100 - assert min_power_discharge[start] == 0 - assert max_power_discharge[start] == 100 + assert min_power_discharge[0] == 0 + assert max_power_discharge[0] == 100 assert storage_unit.outputs["energy"][start] == 0 orderbook = [ @@ -187,7 +191,7 @@ def test_storage_feedback(storage_unit, mock_market_config): "only_hours": None, "price": cost_discharge, "accepted_price": cost_discharge, - "accepted_volume": max_power_discharge[start] / 2, + "accepted_volume": max_power_discharge[0] / 2, } ] # max_power_charge gets accepted @@ -200,9 +204,9 @@ def test_storage_feedback(storage_unit, mock_market_config): ) # we do not need additional min_power, as our runtime requirement is fulfilled - assert min_power_discharge[start] == 0 + assert min_power_discharge[0] == 0 # we can not bid the maximum anymore, because we already provide energy on the other market - assert max_power_discharge[start] == 50 + assert max_power_discharge[0] == 50 storage_unit.execute_current_dispatch(start, end) # second market request for next interval @@ -213,8 +217,8 @@ def test_storage_feedback(storage_unit, mock_market_config): ) # now we can bid max_power and need min_power again - assert min_power_discharge[start] == 0 - assert max_power_discharge[start] == 100 + assert min_power_discharge[0] == 0 + assert max_power_discharge[0] == 100 def test_storage_ramping(storage_unit): @@ -235,18 +239,16 @@ def test_storage_ramping(storage_unit): start, end, product_type="energy" ) - assert min_power_charge[start] == 0 - assert max_power_charge[start] == -100 + assert min_power_charge[0] == 0 + assert max_power_charge[0] == -100 - assert min_power_discharge[start] == 0 - assert max_power_discharge[start] == 100 + assert min_power_discharge[0] == 0 + assert max_power_discharge[0] == 100 max_ramp_discharge = storage_unit.calculate_ramp_discharge( - 500, 0, max_power_discharge[start] - ) - max_ramp_charge = storage_unit.calculate_ramp_charge( - 500, 0, max_power_charge[start] + 500, 0, max_power_discharge[0] ) + max_ramp_charge = storage_unit.calculate_ramp_charge(500, 0, max_power_charge[0]) assert max_ramp_discharge == 60 assert max_ramp_charge == -60 @@ -265,11 +267,9 @@ def test_storage_ramping(storage_unit): end = product_tuple[1] max_ramp_discharge = storage_unit.calculate_ramp_discharge( - 500, 60, max_power_discharge[start] - ) - max_ramp_charge = storage_unit.calculate_ramp_charge( - 500, 60, max_power_charge[start] + 500, 60, max_power_discharge[0] ) + max_ramp_charge = storage_unit.calculate_ramp_charge(500, 60, max_power_charge[0]) assert max_ramp_discharge == 100 assert max_ramp_charge == -60 @@ -288,11 +288,9 @@ def test_storage_ramping(storage_unit): end = product_tuple[1] max_ramp_discharge = storage_unit.calculate_ramp_discharge( - 500, -60, max_power_discharge[start] - ) - max_ramp_charge = storage_unit.calculate_ramp_charge( - 500, -60, max_power_charge[start] + 500, -60, max_power_discharge[0] ) + max_ramp_charge = storage_unit.calculate_ramp_charge(500, -60, max_power_charge[0]) assert max_ramp_discharge == 60 assert max_ramp_charge == -100 @@ -312,7 +310,7 @@ def test_execute_dispatch(storage_unit): # dispatch full discharge dispatched_energy = storage_unit.execute_current_dispatch(start, end) - assert dispatched_energy.iloc[0] == 100 + assert dispatched_energy[0] == 100 assert math.isclose( storage_unit.outputs["soc"][end], 500 - 100 / storage_unit.efficiency_discharge, @@ -322,7 +320,7 @@ def test_execute_dispatch(storage_unit): storage_unit.outputs["energy"][start] = -100 storage_unit.outputs["soc"][start] = 0.5 * storage_unit.max_soc dispatched_energy = storage_unit.execute_current_dispatch(start, end) - assert dispatched_energy.iloc[0] == -100 + assert dispatched_energy[0] == -100 assert math.isclose( storage_unit.outputs["soc"][end], 500 + 100 * storage_unit.efficiency_charge, @@ -332,14 +330,14 @@ def test_execute_dispatch(storage_unit): storage_unit.outputs["soc"][start] = 0.05 * storage_unit.max_soc dispatched_energy = storage_unit.execute_current_dispatch(start, end) assert math.isclose( - dispatched_energy.iloc[0], 50 * storage_unit.efficiency_discharge, abs_tol=0.1 + dispatched_energy[0], 50 * storage_unit.efficiency_discharge, abs_tol=0.1 ) # adjust dispatch to soc limit for charging storage_unit.outputs["energy"][start] = -100 storage_unit.outputs["soc"][start] = 0.95 * storage_unit.max_soc dispatched_energy = storage_unit.execute_current_dispatch(start, end) assert math.isclose( - dispatched_energy.iloc[0], -50 / storage_unit.efficiency_charge, abs_tol=0.1 + dispatched_energy[0], -50 / storage_unit.efficiency_charge, abs_tol=0.1 ) assert math.isclose( storage_unit.outputs["soc"][end], storage_unit.max_soc, abs_tol=0.001 @@ -350,7 +348,7 @@ def test_execute_dispatch(storage_unit): end = end + storage_unit.index.freq storage_unit.outputs["energy"][start] = -100 dispatched_energy = storage_unit.execute_current_dispatch(start, end) - assert dispatched_energy.iloc[0] == 0 + assert dispatched_energy[0] == 0 assert math.isclose( storage_unit.outputs["soc"][end], storage_unit.max_soc, abs_tol=0.001 ) diff --git a/tests/test_units.py b/tests/test_units.py index f1539de63..5b951035c 100644 --- a/tests/test_units.py +++ b/tests/test_units.py @@ -7,15 +7,26 @@ import pandas as pd from assume.common.base import SupportsMinMax, SupportsMinMaxCharge +from assume.common.forecasts import NaiveForecast def test_minmax(): - mm = SupportsMinMax("Test", "TestOperator", "TestTechnology", {}, None, "empty") + index = pd.date_range("2022-01-01", periods=24, freq="h") + forecaster = NaiveForecast(index, availability=1, price_forecast=50) + + mm = SupportsMinMax( + id="Test", + unit_operator="TestOperator", + technology="TestTechnology", + bidding_strategies={}, + forecaster=forecaster, + node="empty", + ) + mm.ramp_down = 200 mm.ramp_up = 400 mm.max_power = 1000 mm.min_power = 200 - mm.index = pd.date_range("2022-01-01", periods=24, freq="h") # stay turned off assert mm.calculate_ramp(op_time=1, previous_power=0, power=0, current_power=0) == 0 @@ -105,8 +116,16 @@ def test_minmax(): def test_minmaxcharge(): + index = pd.date_range("2022-01-01", periods=24, freq="h") + forecaster = NaiveForecast(index, availability=1, price_forecast=50) + mmc = SupportsMinMaxCharge( - "Test", "TestOperator", "TestTechnology", {}, None, "empty" + id="Test", + unit_operator="TestOperator", + technology="TestTechnology", + bidding_strategies={}, + forecaster=forecaster, + node="empty", ) mmc.ramp_down_charge = -100 @@ -141,8 +160,16 @@ def test_minmaxcharge(): def test_minmaxcharge_unconstrained(): + index = pd.date_range("2022-01-01", periods=24, freq="h") + forecaster = NaiveForecast(index, availability=1, price_forecast=50) + mmc = SupportsMinMaxCharge( - "Test", "TestOperator", "TestTechnology", {}, None, "empty" + id="Test", + unit_operator="TestOperator", + technology="TestTechnology", + bidding_strategies={}, + forecaster=forecaster, + node="empty", ) # 1. wenn ramp is undefined, it should not create constraints @@ -169,12 +196,20 @@ def test_minmaxcharge_unconstrained(): def test_minmax_operationtime(): - mm = SupportsMinMax("Test", "TestOperator", "TestTechnology", {}, None, "empty") - mm.index = pd.date_range( - start=datetime(2023, 7, 1), - end=datetime(2023, 7, 2), - freq="1h", + index = pd.date_range( + start=datetime(2023, 7, 1), end=datetime(2023, 7, 2), freq="1h" + ) + forecaster = NaiveForecast(index, availability=1, price_forecast=50) + + mm = SupportsMinMax( + id="Test", + unit_operator="TestOperator", + technology="TestTechnology", + bidding_strategies={}, + forecaster=forecaster, + node="empty", ) + mm.outputs["energy"] += 500 mm.min_down_time = 4 mm.min_operating_time = 4 diff --git a/tests/test_units_operator.py b/tests/test_units_operator.py index 21d6385c1..0f939ec15 100644 --- a/tests/test_units_operator.py +++ b/tests/test_units_operator.py @@ -12,6 +12,7 @@ from mango.util.clock import ExternalClock from mango.util.termination_detection import tasks_complete_or_sleeping +from assume.common.fast_pandas import FastIndex from assume.common.forecasts import NaiveForecast from assume.common.market_objects import MarketConfig, MarketProduct from assume.common.units_operator import UnitsOperator @@ -47,7 +48,7 @@ async def units_operator() -> UnitsOperator: units_agent.add_role(units_role) agent_id = container.register(units_agent) - index = pd.date_range(start=start, end=end + pd.Timedelta(hours=4), freq="1h") + index = FastIndex(start=start, end=end + pd.Timedelta(hours=4), freq="1h") params_dict = { "bidding_strategies": {"EOM": NaiveSingleBidStrategy()}, @@ -57,7 +58,7 @@ async def units_operator() -> UnitsOperator: "min_power": 0, "forecaster": NaiveForecast(index, demand=1000), } - unit = Demand("testdemand", index=index, **params_dict) + unit = Demand("testdemand", **params_dict) units_role.add_unit(unit) start_ts = datetime2timestamp(start) @@ -87,7 +88,7 @@ async def rl_units_operator() -> RLUnitsOperator: units_agent.add_role(units_role) agent_id = container.register(units_agent) - index = pd.date_range(start=start, end=end + pd.Timedelta(hours=4), freq="1h") + index = FastIndex(start=start, end=end + pd.Timedelta(hours=4), freq="1h") params_dict = { "bidding_strategies": {"EOM": NaiveSingleBidStrategy()}, @@ -97,7 +98,7 @@ async def rl_units_operator() -> RLUnitsOperator: "min_power": 0, "forecaster": NaiveForecast(index, demand=1000), } - unit = Demand("testdemand", index=index, **params_dict) + unit = Demand("testdemand", **params_dict) units_role.add_unit(unit) start_ts = datetime2timestamp(start) @@ -168,7 +169,7 @@ async def test_write_learning_params(rl_units_operator: RLUnitsOperator): marketconfig = rl_units_operator.available_markets[0] start = datetime(2020, 1, 1) end = datetime(2020, 1, 2) - index = pd.date_range(start=start, end=end + pd.Timedelta(hours=24), freq="1h") + index = FastIndex(start=start, end=end + pd.Timedelta(hours=24), freq="1h") params_dict = { "bidding_strategies": { @@ -183,7 +184,7 @@ async def test_write_learning_params(rl_units_operator: RLUnitsOperator): "min_power": 0, "forecaster": NaiveForecast(index, powerplant=1000), } - unit = PowerPlant("testplant", index=index, **params_dict) + unit = PowerPlant("testplant", **params_dict) rl_units_operator.add_unit(unit) rl_units_operator.learning_mode = True @@ -243,24 +244,28 @@ async def test_get_actual_dispatch(units_operator: UnitsOperator): market_dispatch, unit_dfs = units_operator.get_actual_dispatch("energy", last) # THEN resulting unit dispatch dataframe contains one row # which is for the current time - as we must know our current dispatch - assert unit_dfs[0].index[0].timestamp() == clock.time - assert len(unit_dfs[0]) == 1 + assert datetime2timestamp(unit_dfs[0]["time"][0]) == last + assert datetime2timestamp(unit_dfs[0]["time"][1]) == clock.time + # only 1 start and stop contained + assert len(unit_dfs[0]["time"]) == 2 assert len(market_dispatch) == 0 # WHEN another hour passes + last = clock.time clock.set_time(clock.time + 3600) - last = clock.time - 3600 # THEN resulting unit dispatch dataframe contains only one row with current dispatch market_dispatch, unit_dfs = units_operator.get_actual_dispatch("energy", last) - assert unit_dfs[0].index[0].timestamp() == clock.time - assert len(unit_dfs[0]) == 1 + assert datetime2timestamp(unit_dfs[0]["time"][0]) == last + assert datetime2timestamp(unit_dfs[0]["time"][1]) == clock.time + assert len(unit_dfs[0]["time"]) == 2 assert len(market_dispatch) == 0 + last = clock.time clock.set_time(clock.time + 3600) - last = clock.time - 3600 market_dispatch, unit_dfs = units_operator.get_actual_dispatch("energy", last) - assert unit_dfs[0].index[0].timestamp() == clock.time - assert len(unit_dfs[0]) == 1 + assert datetime2timestamp(unit_dfs[0]["time"][0]) == last + assert datetime2timestamp(unit_dfs[0]["time"][1]) == clock.time + assert len(unit_dfs[0]["time"]) == 2 assert len(market_dispatch) == 0 diff --git a/tests/test_utils.py b/tests/test_utils.py index 5ad8538e6..cda27d344 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-or-later import calendar +import time from datetime import datetime, timedelta, timezone from unittest.mock import patch @@ -11,6 +12,7 @@ from dateutil import rrule as rr from dateutil.tz import tzlocal +from assume.common.fast_pandas import FastIndex, FastSeries from assume.common.market_objects import MarketConfig, MarketProduct from assume.common.utils import ( aggregate_step_amount, @@ -19,6 +21,7 @@ get_available_products, get_products_index, initializer, + parse_duration, plot_orderbook, separate_orders, timestamp2datetime, @@ -447,6 +450,191 @@ def test_datetime2timestamp(): assert 0 == datetime2timestamp(unix_start) +def test_create_date_range(): + start = datetime(2020, 1, 1, 0) + end = datetime(2020, 1, 1, 5) + n = 1000 + index = FastIndex(start, end, freq="1h") + fs = FastSeries(index) + + t = time.time() + for i in range(n): + index = FastIndex(start, end, freq="1h") + fs = FastSeries(index) + res = time.time() - t + + t = time.time() + for i in range(n): + q_pd = pd.date_range(start, end, freq="1h") + res_pd = time.time() - t + # this is sometimes faster, sometimes not + # as a lot of objects are created + assert res < res_pd + 0.1 + + new_end = datetime(2020, 1, 1, 3) + + # check that slicing is faster + t = time.time() + for i in range(n): + q_slice = fs[start:new_end] + res_slice = time.time() - t + + series = pd.Series(0, index=q_pd) + + t = time.time() + for i in range(n): + q_pd_slice = series[start:new_end] + res_slice_pd = time.time() - t + # more than at least factor 5 + assert res_slice < res_slice_pd / 5 + + # check that setting items is faster: + t = time.time() + for i in range(n): + fs[start] = 1 + res_slice = time.time() - t + + series = pd.Series(0, index=q_pd) + + t = time.time() + for i in range(n): + series[start] = 1 + res_slice_pd = time.time() - t + # more than at least factor 5 + assert res_slice < res_slice_pd / 5 + + # check that setting slices is faster + t = time.time() + for i in range(n): + fs[start:new_end] = 17 + res_slice = time.time() - t + + series = pd.Series(0, index=q_pd) + + t = time.time() + for i in range(n): + series[start:new_end] = 17 + res_slice_pd = time.time() - t + # more than at least factor 5 + assert res_slice < res_slice_pd / 5 + + se = pd.Series(0.0, index=fs.index.get_date_list()) + se.loc[start] + + series.loc[new_end] = 33 + + fs[new_end] = 33 + new = series[start:new_end][::-1] + assert new.iloc[0] == 33 + new = fs[start:new_end][::-1] + assert new[0] == 33 + fs.data + fs.index._get_idx_from_date(start) + fs.index._get_idx_from_date(new_end) + fs.data[0:4] + + +def test_convert_pd(): + start = datetime(2020, 1, 1, 0) + end = datetime(2020, 1, 1, 5) + index = FastIndex(start, end, freq="1h") + fs = FastSeries(index) + + df = fs.as_df() + assert isinstance(df, pd.DataFrame) + + +def test_set_list(): + start = datetime(2020, 1, 1, 0) + end = datetime(2020, 2, 1, 5) + n = 1000 + index = FastIndex(start, end, freq="1h") + fs = FastSeries(index) + + dr = pd.date_range(start, end=datetime(2020, 1, 3, 5)) + dr = pd.Series(dr) + + series = pd.Series(0, index=pd.date_range(start, end, freq="1h")) + + t = time.time() + for i in range(n): + result_pd = fs[dr] + res_fds = time.time() - t + f_series = series > 1 + series[f_series] = 4 + fs.data[f_series] = 4 + + # accessing lists or series elements is also faster + # check getting list or series + t = time.time() + for i in range(n): + result = series[dr] + res_pd = time.time() - t + print(res_fds) + print(res_pd) + assert res_fds < res_pd + + # check setting list or series with single value + t = time.time() + for i in range(n): + fs[dr] = 3 + res_fds = time.time() - t + + t = time.time() + for i in range(n): + series[dr] = 3 + res_pd = time.time() - t + print(res_fds) + print(res_pd) + assert res_fds < res_pd + + # check setting list or series with a series + d_new = pd.Series(dr.index) + + t = time.time() + for i in range(n): + fs[dr] = d_new + res_fds = time.time() - t + + t = time.time() + for i in range(n): + series[dr] = d_new + res_pd = time.time() - t + print(res_fds) + print(res_pd) + assert res_fds < res_pd + + # check setting list or series with a list + d_new = list(d_new) + + t = time.time() + for i in range(n): + fs[dr] = d_new + res_fds = time.time() - t + + t = time.time() + for i in range(n): + series[dr] = d_new + res_pd = time.time() - t + print(res_fds) + print(res_pd) + assert res_fds < res_pd + + +def test_parse_duration(): + assert parse_duration("24h") == timedelta(days=1) + assert parse_duration("1d") == timedelta(days=1) + assert parse_duration("12h") == timedelta(hours=12) + assert parse_duration("0.25h") == timedelta(minutes=15) + assert parse_duration("15m") == timedelta(minutes=15) + assert parse_duration("1m") == timedelta(minutes=1) + assert parse_duration("10s") == timedelta(seconds=10) + with pytest.raises(ValueError): + parse_duration("1") + with pytest.raises(ValueError): + parse_duration("100ms") + + if __name__ == "__main__": test_convert_rrule() test_available_products() diff --git a/tests/utils.py b/tests/utils.py index 8add9a9be..07c0275be 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -6,7 +6,6 @@ from itertools import product import numpy as np -import pandas as pd from assume.common.market_objects import Order @@ -142,8 +141,5 @@ def get_test_prices(num: int = 24): prices = dict( power=power_price, gas=gas, co2=co2, lignite=lignite, coal=coal, nuclear=nuclear ) - prices = pd.DataFrame( - data=prices, index=pd.date_range(start="2018-01-01", freq="h", periods=num) - ) return prices