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