def train(data, V=8, M=4, subquantizer_clusters=256, parameters=None,
          kmeans_coarse_iters=10, kmeans_local_iters=20, n_init=10,
          subquantizer_sample_ratio=1.0, random_state=None, verbose=False):
    """
    Fit an LOPQ model.

    :param ndarray data:
        a NxD matrix of training data points with observations on the rows

    :param int V:
        number of coarse clusters
    :param int M:
        number of fine codes; same as number of bytes per compressed
        vector in memory with 256 subquantizer clusters
    :param int subquantizer_clusters:
        the number of clusters for each subquantizer
    :param tuple parameters:
        a tuple of parameters - missing parameters are allowed to be None

    :param int kmeans_coarse_iters:
        kmeans iterations
    :param int kmeans_local_iters:
        kmeans iterations for subquantizers
    :param int n_init:
        the number of independent kmeans runs for all kmeans when training - set low for faster training
    :param float subquantizer_sample_ratio:
        the proportion of the training data to sample for training subquantizers - since the number of
        subquantizer clusters is much smaller then the number of coarse clusters, less data is needed
    :param int random_state:
        a random seed used in all random operations during training if provided
    :param bool verbose:
        a bool enabling verbose output during training

    :returns tuple:
        a tuple of model parameters that can be used to instantiate an LOPQModel object
    """

    # Set logging level for verbose mode
    if (verbose):
        logger.setLevel(logging.DEBUG)

    # Extract parameters
    Cs = Rs = mus = subquantizers = None
    if parameters is not None:
        Cs, Rs, mus, subquantizers = parameters

    # Enforce parameter dependencies
    if Rs is None or mus is None:
        Rs = mus = None

    # Split vectors
    # TODO: permute dims here if this hasn't already been done
    first_half, second_half = np.split(data, 2, axis=1)

    # Cluster coarse splits
    if Cs is not None:
        logger.info('Using existing coarse quantizers.')
        C1, C2 = Cs
    else:
        C1 = train_coarse(first_half, V, kmeans_coarse_iters, n_init, random_state)
        C2 = train_coarse(second_half, V, kmeans_coarse_iters, n_init, random_state)

    # Compute local rotations
    if Rs is not None and mus is not None:
        logger.info('Using existing rotations.')
        Rs1, Rs2 = Rs
        mu1, mu2 = mus
        assignments1 = assignments2 = residuals1 = residuals2 = None
    else:
        Rs1, mu1, assignments1, residuals1 = compute_local_rotations(first_half, C1, M / 2)
        Rs2, mu2, assignments2, residuals2 = compute_local_rotations(second_half, C2, M / 2)

    # Subquantizers don't need as much data, so we could sample here
    subquantizer_sample_ratio = min(subquantizer_sample_ratio, 1.0)
    N = data.shape[0]
    N2 = int(np.floor(subquantizer_sample_ratio * N))
    sample_inds = np.random.RandomState(random_state).choice(N, N2, False)
    logger.info('Sampled training data for subquantizers with %f proportion (%d points).' % (subquantizer_sample_ratio, N2))

    # Use assignments and residuals from rotation computation if available
    if assignments1 is not None:
        residuals1 = residuals1[sample_inds]
        residuals2 = residuals2[sample_inds]
        assignments1 = assignments1[sample_inds]
        assignments2 = assignments2[sample_inds]
    else:
        residuals1, assignments1 = compute_residuals(first_half[sample_inds], C1)
        residuals2, assignments2 = compute_residuals(second_half[sample_inds], C2)

    # Project residuals
    logger.info('Projecting residuals to local frame...')
    projected1 = project_residuals_to_local(residuals1, assignments1, Rs1, mu1)
    projected2 = project_residuals_to_local(residuals2, assignments2, Rs2, mu2)

    logger.info('Fitting subquantizers...')
    subquantizers1 = train_subquantizers(projected1, M / 2, subquantizer_clusters, kmeans_local_iters, n_init, random_state=random_state)
    subquantizers2 = train_subquantizers(projected2, M / 2, subquantizer_clusters, kmeans_local_iters, n_init, random_state=random_state)
    logger.info('Done fitting subquantizers.')

    return (C1, C2), (Rs1, Rs2), (mu1, mu2), (subquantizers1, subquantizers2)

########################################
# Model class
########################################

# Named tuple type for LOH codes
dataset/ETHPy150Open yahoo/lopq/python/lopq/model.py/train/original