Coverage for rivapy/instruments/ppa_specification.py: 80%
80 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-05 14:27 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-05 14:27 +0000
1from typing import Union, Tuple, Iterable, Set, List
2import abc
3import numpy as np
4import pandas as pd
5import datetime as dt
6import rivapy.tools.interfaces as interfaces
8# from rivapy.instruments.factory import create as _create
9from rivapy.tools.factory import create as _create
10from rivapy.tools.datetime_grid import DateTimeGrid
11from rivapy.tools import SimpleSchedule
14class PPASpecification(interfaces.FactoryObject):
15 def __init__(
16 self, udl: str, amount: Union[float, np.ndarray], schedule: Union[SimpleSchedule, List[dt.datetime]], fixed_price: float, id: str = None
17 ):
18 """Specification for a simple power purchase agreement (PPA).
20 Args:
21 udl (str): Name of underlying (power) that is delivered (just use for consistency checking within pricing against simulated model values).
22 amount (Union[None, float, np.ndarray]): Amount of power delivered at each timepoint/period. Either a single value s.t. all volumes delivered are constant or a load table. If None, a non-constant amount (e.g. by production from renewables) is assumed.
23 schedule (Union[SimpleSchedule, List[dt.datetime]): Schedule describing when power is delivered.
24 fixed_price (float): The fixed price paif for the power.
25 id (str): Simple id of the specification. If None, a uuid will be generated. Defaults to None.
26 """
27 self.id = id
28 self.udl = udl
29 if id is None:
30 self.id = type(self).__name__ + "/" + str(dt.datetime.now())
31 self.amount = amount
32 if isinstance(schedule, dict): # if schedule is a dict we try to create it from factory
33 self.schedule = _create(schedule)
34 else:
35 self.schedule = schedule
36 self.fixed_price = fixed_price
37 if isinstance(schedule, list):
38 self._schedule_df = pd.DataFrame({"dates": self.schedule}).reset_index()
39 else:
40 self._schedule_df = self.schedule.get_df().set_index(["dates"]).sort_index()
41 self._schedule_df["amount"] = amount
42 self._schedule_df["flow"] = None
44 @staticmethod
45 def _create_sample(n_samples: int, seed: int = None, ref_date=None):
46 schedules = SimpleSchedule._create_sample(n_samples, seed, ref_date)
47 result = []
48 for schedule in schedules:
49 amount = np.random.uniform(low=50.0, high=100.0)
50 fixed_price = np.random.uniform(low=0.5, high=1.5)
51 result.append(PPASpecification(udl="Power", amount=amount, schedule=schedule, fixed_price=fixed_price))
52 return result
54 def get_schedule(self) -> List[dt.datetime]:
55 if not isinstance(self.schedule, list):
56 return self.schedule.get_schedule()
57 return self.schedule
59 def _to_dict(self) -> dict:
60 try: # if isinstance(self.schedule, interfaces.FactoryObject):
61 schedule = self.schedule.to_dict()
62 except Exception as e:
63 schedule = self.schedule
64 return {"udl": self.udl, "id": self.id, "amount": self.amount, "schedule": schedule, "fixed_price": self.fixed_price}
66 def set_amount(self, amount):
67 self.amount = amount
68 self._schedule_df["amount"] = amount
70 def n_deliveries(self):
71 return self._schedule_df.shape[0]
73 def compute_flows(self, refdate: dt.datetime, pfc, result: pd.DataFrame = None, result_col=None) -> pd.DataFrame:
74 df = pfc.get_df()
75 if result is None:
76 self._schedule_df["flow"] = self._schedule_df["amount"] * (df.loc[self._schedule_df.index]["values"] - self.fixed_price)
77 return self._schedule_df
78 result[result_col] = self._schedule_df["amount"] * (df.loc[self._schedule_df.index]["values"] - self.fixed_price)
81class GreenPPASpecification(PPASpecification):
82 def __init__(
83 self,
84 schedule: Union[SimpleSchedule, List[dt.datetime]],
85 fixed_price: float,
86 max_capacity: float,
87 technology: str,
88 udl: str,
89 location: str = None,
90 id: str = None,
91 ):
92 """:term:`Specification` for a green power purchase agreement.
94 In contrast to a normal PPA the quantities of this PPA are related to some kind of
95 renewable energy such as wind or solar, i.e. the quantity is related to some uncertain production.
97 Args:
98 schedule (Union[SimpleSchedule, List[dt.datetime]]): Delivery schedule.
99 fixed_price (float): Fixed price paid for the power.
100 max_capacity (float): The absolute maximal capacity of the renewable energy source. This is used to derive the production amount of the plant by multiplying forecasts with the factor max_capacity/total_capacity (where total capacity may be time dependent).
101 technology (str): Identifier for the technology. This is used to retrieve the simulated values for production of this technology from a model
102 location (str, optional): Identifier for the location. This is used to retrieve the simulated values for production of this technology at this location from a model that supports this feature. Defaults to None.
103 udl (str, optional): Name of underlying (power) that is delivered (just use for consistency checking within pricing against simulated model values). It is used within pricing when the respective simulated price must be retrieved from a model's simulation results.
104 id (str, optional): Unique identifier of this contract. Defaults to None.
105 """
106 super().__init__(udl, None, schedule, fixed_price, id)
107 self.technology = technology
108 self.max_capacity = max_capacity
109 self.location = location
111 @staticmethod
112 def _create_sample(n_samples: int, seed: int = None, ref_date=None):
113 schedules = SimpleSchedule._create_sample(n_samples, seed, ref_date)
114 result = []
115 for schedule in schedules:
116 max_capacity = np.random.uniform(low=50.0, high=100.0)
117 fixed_price = np.random.uniform(low=0.5, high=1.5)
118 result.append(
119 GreenPPASpecification(
120 udl="Power", technology="Wind", location="Onshore", fixed_price=fixed_price, max_capacity=max_capacity, schedule=schedule
121 )
122 )
123 return result
125 def _to_dict(self) -> dict:
126 result = super()._to_dict()
127 del result["amount"]
128 result["technology"] = self.technology
129 result["max_capacity"] = self.max_capacity
130 result["location"] = self.location
131 return result
133 def compute_flows(self, refdate: dt.datetime, pfc, forecast_amount: np.ndarray, result: pd.DataFrame = None, result_col=None) -> pd.DataFrame:
134 self.set_amount(forecast_amount)
135 return super().compute_flows(refdate, pfc, result, result_col)