def lower_bound_improved(X_train, X_test, region):
    r"""Compute the "LB_Improved" lower bounds between two datasets.

    Parameters
    ----------
    X_train : array-like, shape = (n_samples_train, n_timestamps)
        Training set.

    X_test: : array-like, shape = (n_samples_test, n_timestamps)
        Test set.

    region : array, shape = (2, n_timestamps)
        Constraint region. The first row consists of the starting indices
        (included) and the second row consists of the ending indices (excluded)
        of the valid rows for each column.

    Returns
    -------
    lower_bounds : array, shape = (n_samples_test, n_samples_train)
        "LB_Improved" lower bounds.

    Notes
    -----
    The "LB_Improved" lower bounds are computed as

    .. math::

        LB_Improved(X, Y) = \sqrt\left\( \Vert X - H(X, Y) \Vert_{2}^2
        + \Vert Y - H(Y, H(X, Y)) \Vert_{2}^2 \right\)

    where :math:`X` is the test set (``X_test``), :math:`Y` is the
    training set (``X_train``), and :math:`H(X, Y)` is the projection
    of :math:`X` on :math:`Y`.

    References
    ----------
    .. [1] D. Lemire, "Faster Retrieval with a Two-Pass Dynamic-Time-Warping
           Lower Bound". Pattern Recognition, 42(9), 2169-2180 (2009).

    Examples
    --------
    >>> X_train = [[0, 1, 2, 3], [1, 2, 3, 4]]
    >>> X_test = [[0, 2.5, 3.5, 3.3]]
    >>> region = [[0, 0, 1, 2], [2, 3, 4, 4]]
    >>> lower_bound_improved(X_train, X_test, region)
    array([[0.76..., 1.11...]])

    """
    X_train = check_array(X_train)
    X_test = check_array(X_test)
    _check_consistent_lengths(X_train, X_test)

    # LB Keogh lower bounds
    lower_train, upper_train = _warping_envelope(X_train, region)
    X_test_proj = _clip(X_test, lower_train, upper_train)
    squared_lb_keogh = np.sum(
        (X_test[:, None, :] - X_test_proj) ** 2, axis=-1
    )

    # LB Improved lower bounds
    lower_test, upper_test = _warping_envelope(X_test_proj, region)
    X_train_proj = _clip(X_train, lower_test, upper_test)
    squared_lb_improved = np.sum(
        (X_train[:, None, :] - X_train_proj) ** 2, axis=-1
    )

    return np.sqrt(squared_lb_keogh + squared_lb_improved.T)
Compute the "LB_Improved" lower bounds between two datasets.

    Parameters
    ----------
    X_train : array-like, shape = (n_samples_train, n_timestamps)
        Training set.

    X_test: : array-like, shape = (n_samples_test, n_timestamps)
        Test set.

    region : array, shape = (2, n_timestamps)
        Constraint region. The first row consists of the starting indices
        (included) and the second row consists of the ending indices (excluded)
        of the valid rows for each column.

    Returns
    -------
    lower_bounds : array, shape = (n_samples_test, n_samples_train)
        "LB_Improved" lower bounds.

    Notes
    -----
    The "LB_Improved" lower bounds are computed as

    .. math::

        LB_Improved(X, Y) = \sqrt\left\( \Vert X - H(X, Y) \Vert_{2}^2
        + \Vert Y - H(Y, H(X, Y)) \Vert_{2}^2 \right\)

    where :math:`X` is the test set (``X_test``), :math:`Y` is the
    training set (``X_train``), and :math:`H(X, Y)` is the projection
    of :math:`X` on :math:`Y`.

    References
    ----------
    .. [1] D. Lemire, "Faster Retrieval with a Two-Pass Dynamic-Time-Warping
           Lower Bound". Pattern Recognition, 42(9), 2169-2180 (2009).

    Examples
    --------
    >>> X_train = [[0, 1, 2, 3], [1, 2, 3, 4]]
    >>> X_test = [[0, 2.5, 3.5, 3.3]]
    >>> region = [[0, 0, 1, 2], [2, 3, 4, 4]]
    >>> lower_bound_improved(X_train, X_test, region)
    array([[0.76..., 1.11...]])

    
pyts/metrics/lower_bounds.py