def compute_attachment(
    xi: List[float],
    xj: List[float],
    wij: List[float],
    start_k: int = 0,
    eps: float = 0.0,
    to_opinion: AttachmentType = AttachmentType.TO_INITIAL
) -> Tuple[List[float], List[Dict[Text, int]]]:
    """Computes the level of attachment to the initial opinion for person i.

    For person i at time k is computed as follows:
    a_{i, i}(k) = \frac{x_i(k+1) - x_i(0) + \epsilon}{x_i(k) - x_i(0) + w_{i, j}(k)\bigg(x_j(k) - x_i(k)\bigg) + \epsilon}

    Args:
        xi: List of opinions for person i over time (multiple rounds).

        xj: List of opinions for person j over time (multiple rounds).

        wij: List of influence from person i to person j.

        start_k: Start value for k to be 0 or 1.

        eps: The small amount to always add to the denominator.

        to_opinion: The type of attachment to either initial or previous.

    Returns:
        Value (level) of attachment to the initial opinion or from previous for
        person i over time, called a_{i, i}. Also note if you want to avoid the
        possibility of division by zero, you should always use a small epsilon
        larger than zero to add to the denominator.

    Raises:
        ValueError: If the length of opinions were not the same or number of 
        influence weights were not one less than the length of opinion vector.
        Also if start_k was given anything but 0 or 1. Also if the type of 
        to_opinion was unkown.
    """
    if len(xi) != len(xj):
        raise ValueError(
            'Length of opinions do not match. xi: {}, xj: {}'.format(xi, xj))
    if len(xi) != len(wij) + 1:
        raise ValueError('Length of opinions and influences do not match. ')
    if start_k != 0 and start_k != 1:
        raise ValueError(
            'Start k should be 0 or 1. It was given {}'.format(start_k))
    opinion_len = len(xi)
    aii_nan_details = []
    aii = []
    for k in range(start_k, opinion_len - 1):
        if to_opinion == AttachmentType.TO_INITIAL:
            xi_k_minus_x0_or_previous_str = 'xi[k]-xi[0]==0'
            numerator = xi[k + 1] - xi[0]
            denominator = xi[k] - xi[0] + wij[k] * (xj[k] - xi[k]) + eps
        elif to_opinion == AttachmentType.TO_PREVIOUS:
            xi_k_minus_x0_or_previous_str = 'xi[k]-xi[k-1]==0'
            if k > 0:
                numerator = xi[k + 1] - xi[k - 1]
                denominator = xi[k] - xi[k -
                                         1] + wij[k] * (xj[k] - xi[k]) + eps
            else:
                numerator = xi[k + 1] - xi[k]
                denominator = wij[k] * (xj[k] - xi[k]) + eps
        else:
            ValueError(
                'Type of attachment was unkown. It was {}'.format(to_opinion))
        # Getting the details about how NaN is happening.
        aii_nan_detail = {}
        if utils.is_almost_zero(numerator) and utils.is_almost_zero(
                denominator):
            aii_nan_detail['0/0'] = 1
        if not utils.is_almost_zero(numerator) and utils.is_almost_zero(
                denominator):
            aii_nan_detail['n/0'] = 1
        if utils.is_almost_zero(denominator) and utils.is_almost_zero(xi[k] -
                                                                      xi[0]):
            aii_nan_detail[xi_k_minus_x0_or_previous_str] = 1
        if utils.is_almost_zero(denominator) and utils.is_almost_zero(wij[k]):
            aii_nan_detail['wij[k]==0'] = 1
        if utils.is_almost_zero(denominator) and utils.is_almost_zero(xj[k] -
                                                                      xi[k]):
            aii_nan_detail['xj[k]-xi[k]==0'] = 1
        if utils.is_almost_zero(denominator) and eps > 0:
            print('Warning: choose a different epsilon.'
                  ' There has been an denominator equals 0'
                  ' with the current one which is {}.'.format(eps))
        # attachment = 0  #  np.nan  << CHECK HERE >> DUE TO NOAH'S CODE IS SET 0.
        attachment = np.nan
        if not utils.is_almost_zero(denominator):
            attachment = numerator / denominator
        aii.append(attachment)
        aii_nan_details.append(aii_nan_detail)
    return aii, aii_nan_details
 def test_is_almost_zero_when_should_be_computationally_zero(self):
     self.assertTrue(utils.is_almost_zero(0.85 - 0.8 + 0.2 * (0.6 - 0.85)))
 def test_is_almost_zero_raises_when_negative_num_of_exponents(self):
     with self.assertRaises(ValueError):
         utils.is_almost_zero(x=0.1, num_of_exponents=-2)
 def test_is_almost_zero_when_exactly_zero(self):
     self.assertTrue(utils.is_almost_zero(x=0.0, num_of_exponents=10))
 def test_is_almost_zero_when_negative_but_small_enough(self):
     self.assertTrue(utils.is_almost_zero(x=-0.00001, num_of_exponents=4))
 def test_is_almost_zero_when_negative_and_not_close_to_zero(self):
     self.assertFalse(utils.is_almost_zero(x=-1, num_of_exponents=4))
 def test_is_almost_zero_when_close_enough_to_zero(self):
     self.assertTrue(utils.is_almost_zero(x=0.000099, num_of_exponents=4))
 def test_is_almost_zero_when_not_close_enough_to_zero(self):
     self.assertFalse(utils.is_almost_zero(x=0.1, num_of_exponents=4))