def _cond_EPU_share(self, FX1, FX2, d1, d2, v1, v2, c): """Returns EPU conditional on given demand and wind generations under a share policy **Parameters**: `FX1` (`ConvGenDistribution`): available conventional generation distribution object for area 1 `FX2` (`ConvGenDistribution`): available conventional generation distribution object for area 2 `d1` (`int`): demand in area 1 `d2` (`int`): demand in area 2 `v1` (`int`): net demand in area 1 (demand - renewable generation) `v2` (`int`): net demand in area 2 `c` (`int`): Interconnector capacity """ return C_CALL.cond_eeu_share_py_interface( np.int32(d1), np.int32(d2), np.int32(v1), np.int32(v2), np.int32(c), np.int32(FX1.min), np.int32(FX2.min), np.int32(FX1.max), np.int32(FX2.max), ffi.cast("double *", FX1.cdf_vals.ctypes.data), ffi.cast("double *", FX2.cdf_vals.ctypes.data), ffi.cast("double *", FX1.expectation_vals.ctypes.data))
def _trapezoid_prob(self, X, ulc, c): """Compute the probability mass of a trapezoidal segment of the plane # The trapezoid is formed by stacking a right triangle on top of a rectangle # where the hypotenuse is facing right **Parameters**: `X` (`BivariateConvGenDist`) bivariate available conventional generation object `ulc` (`list`): upper left corner `c` (`int`): width of trapezoid """ ulc1, ulc2 = ulc return C_CALL.trapezoid_prob_py_interface( np.int32(ulc1), np.int32(ulc2), np.int32(c), np.int32(X.X1.min), np.int32(X.X2.min), np.int32(X.X1.max), np.int32(X.X2.max), ffi.cast("double *", X.X1.cdf_vals.ctypes.data), ffi.cast("double *", X.X2.cdf_vals.ctypes.data))
def _triangle_prob(bigen, origin, length): """Recursive calculation of probability mass for the interior of a right, symmetric triangular lattice. This function is vestigial from previous package versions and is here only to test it; no method in this class uses this function anymore and instead call _trapezoid_prob **Parameters**: `bigen` (`BivariateConvGenDist`) bivariate available conventional generation object `origin` (`list`): right angle coordinate in the plane `length` (`int`): length of triangle legs """ origin = np.ascontiguousarray(origin, dtype=np.int32) return C_CALL.triangle_prob_py_interface( np.int32(origin[0]), np.int32(origin[1]), np.int32(length), np.int32(bigen.X1.min), np.int32(bigen.X2.min), np.int32(bigen.X1.max), np.int32(bigen.X2.max), ffi.cast("double *", bigen.X1.cdf_vals.ctypes.data), ffi.cast("double *", bigen.X2.cdf_vals.ctypes.data))
def cdf(self, m, c=0, policy="share", get_pointwise_risk=False): """Evaluate the CDF of bivariate power margins for a given system configuration under hindcast. **Parameters**: `m` (`tuple`, `list`, or `numpy.ndarray`) point to evaluate in power margin space `c` (`int`): Interconnector capacity `policy` (`str`): Either 'share' or 'veto' `get_pointwise_risk` (`str`): return pandas DataFrame with shortfall probabilities induced by each historic observation """ m = np.clip(m, a_min=-self.MARGIN_BOUND, a_max=self.MARGIN_BOUND) m1, m2 = m #self._check_null_fc() gendist1, gendist2 = self.gen_dists #X2 = self.gen_dists[1] #X = BivariateConvGenDist(X1,X2) #system-wide conv. gen. distribution n = self.n cdf = 0 if get_pointwise_risk: nd0 = [] nd1 = [] cdf_list = [] for i in range(n): v1, v2 = self.net_demand[i, :] d1, d2 = self.demand[i, :] point_cdf = C_CALL.cond_bivariate_power_margin_cdf_py_interface( np.int32(gendist1.min), np.int32(gendist2.min), np.int32(gendist1.max), np.int32(gendist2.max), ffi.cast("double *", gendist1.cdf_vals.ctypes.data), ffi.cast("double *", gendist2.cdf_vals.ctypes.data), np.int32(m1), np.int32(m2), np.int32(v1), np.int32(v2), np.int32(d1), np.int32(d2), np.int32(c), np.int32(policy == "share")) #print("point cdf: {x}, index: {i}".format(x=point_cdf, i=i)) cdf += point_cdf #print(v1) if get_pointwise_risk: nd0.append(v1) nd1.append(v2) cdf_list.append(point_cdf) if get_pointwise_risk: pw_df = pd.DataFrame({"nd0": nd0, "nd1": nd1, "value": cdf_list}) #print(pw_df) return pw_df else: return cdf / n
def simulate_conditional(self, n, cond_value, cond_axis, c, policy, seed=1): """ Simulate power margins in one area conditioned to a particular value in the other area **Parameters**: `n` (`int`): number of simulated values `cond_value` (`int`): conditioning power margin value `cond_axis` (`int`): Conditioning component `c` (`tuple`): Interconnection capacity `policy` (`str`): Either 'share' or 'veto' `seed` (`int`): random seed """ np.random.seed(seed) m1 = np.clip(cond_value, a_min=-self.MARGIN_BOUND, a_max=self.MARGIN_BOUND) m2 = self.MARGIN_BOUND if cond_axis == 1: self._swap_axes() X1 = self.gen_dists[0] X2 = self.gen_dists[1] X = BivariateConvGenDist(X1, X2) #system-wide conv. gen. distribution simulated = np.ascontiguousarray(np.zeros((n, 2)), dtype=np.int32) ### calculate conditional probability of each historical observation given ### margin value tuple m df = self.cdf(m=(m1, m2), c=c, policy=policy, get_pointwise_risk=True) df["value"] = df["value"] - self.cdf( m=(m1 - 1, m2), c=c, policy=policy, get_pointwise_risk=True)["value"] df["d0"] = self.demand[:, 0] df["d1"] = self.demand[:, 1] ## rounding errors can make probabilities negative of the order of 1e-60 df = df.query("value > 0") df = df.sort_values(by="value", ascending=True) probs = df["value"] total_prob = np.sum(probs) if total_prob <= 1e-8: raise Exception( "Region has probability lower than 1e-8; too small to simulate accurately" ) else: probs = np.array(probs) / total_prob df["row_weights"] = np.random.multinomial(n=n, pvals=probs, size=1).reshape( (df.shape[0], )) ## only pass rows which induce at least one simulated value df = df.query("row_weights > 0") row_weights = np.ascontiguousarray(df["row_weights"], dtype=np.int32) net_demand = np.ascontiguousarray(df[["nd0", "nd1"]], dtype=np.int32) demand = np.ascontiguousarray(df[["d0", "d1"]], dtype=np.int32) C_CALL.conditioned_simulation_py_interface( np.int32(n), ffi.cast("int *", simulated.ctypes.data), np.int32(X.X1.min), np.int32(X.X2.min), np.int32(X.X1.max), np.int32(X.X2.max), ffi.cast("double *", X.X1.cdf_vals.ctypes.data), ffi.cast("double *", X.X2.cdf_vals.ctypes.data), ffi.cast("int *", net_demand.ctypes.data), ffi.cast("int *", demand.ctypes.data), ffi.cast("int *", row_weights.ctypes.data), np.int32(net_demand.shape[0]), np.int32(m1), np.int32(c), int(seed), int(policy == "share")) if cond_axis == 1: self._swap_axes() return simulated[:, 1] #first column has variable conditioned on (constant value)
def simulate_region(self, n, m, c, policy, intersection=True, seed=1): """ Simulate region of post interconnector power margins **Parameters**: `n` (`int`): number of simulations `m` (`tuple`): Upper bound that delimits the region for each component `c` (`tuple`): Interconnection capacity `policy` (`str`): Either 'share' or 'veto' `intersection` (`bool`): if `True`, simulate from region given by `m[0] <= m_0 AND m[1] <= m_1` inequality; otherwise from region `m[0] <= m_0 OR m[1] <= m_1` `seed` (`int`): random seed """ def get_prob_df(m, c, policy, intersection): if intersection: df = self.cdf(m=m, c=c, policy=policy, get_pointwise_risk=True) else: if m[0] >= self.MARGIN_BOUND or m[1] >= self.MARGIN_BOUND: # the union of anything with constraint <= infinity is the whole plane df = self.cdf(m=(self.MARGIN_BOUND, self.MARGIN_BOUND), c=c, policy=policy, get_pointwise_risk=True) else: df1 = self.cdf(m=(self.MARGIN_BOUND, m[1]), c=c, policy=policy, get_pointwise_risk=True) df2 = self.cdf(m=(m[0], self.MARGIN_BOUND), c=c, policy=policy, get_pointwise_risk=True) df3 = self.cdf(m=m, c=c, policy=policy, get_pointwise_risk=True) union_prob = df1["value"] + df2["value"] - df3["value"] df = pd.DataFrame({ "value": union_prob, "nd0": df3["nd0"], "nd1": df3["nd1"] }) df["d0"] = self.demand[:, 0] df["d1"] = self.demand[:, 1] df = df.sort_values( by="value", ascending=True ) #.query("value >= 0") #sometimes rounding errors may # produce negative probabilities in the order of -1e-60 #print(df) return df np.random.seed(seed) m = np.clip(m, a_min=-self.MARGIN_BOUND, a_max=self.MARGIN_BOUND) m1, m2 = m X1 = self.gen_dists[0] X2 = self.gen_dists[1] X = BivariateConvGenDist(X1, X2) #system-wide conv. gen. distribution simulated = np.ascontiguousarray(np.zeros((n, 2)), dtype=np.int32) ### calculate conditional probability of each historical observation given ### margin value tuple m df = get_prob_df(m=m, c=c, policy=policy, intersection=intersection) probs = df["value"] total_prob = np.sum(probs) if total_prob <= 1e-8: raise Exception( "Region has probability lower than 1e-8; too small to simulate accurately" ) else: probs = np.array(probs) / total_prob df["row_weights"] = np.random.multinomial(n=n, pvals=probs, size=1).reshape( (df.shape[0], )) ## only pass rows which induce at least one simulated value df = df.query("row_weights > 0") row_weights = np.ascontiguousarray(df["row_weights"], dtype=np.int32) net_demand = np.ascontiguousarray(df[["nd0", "nd1"]], dtype=np.int32) demand = np.ascontiguousarray(df[["d0", "d1"]], dtype=np.int32) C_CALL.region_simulation_py_interface( np.int32(n), ffi.cast("int *", simulated.ctypes.data), np.int32(X.X1.min), np.int32(X.X2.min), np.int32(X.X1.max), np.int32(X.X2.max), ffi.cast("double *", X.X1.cdf_vals.ctypes.data), ffi.cast("double *", X.X2.cdf_vals.ctypes.data), ffi.cast("int *", net_demand.ctypes.data), ffi.cast("int *", demand.ctypes.data), ffi.cast("int *", row_weights.ctypes.data), np.int32(net_demand.shape[0]), np.int32(m1), np.int32(m2), np.int32(c), int(seed), int(intersection), int(policy == "share")) return simulated