Module adnmtf.nmtf_core

Non-negative matrix and tensor factorization core functions

Expand source code
"""Non-negative matrix and tensor factorization core functions

"""

# Author: Paul Fogel

# License: MIT
# Jan 4, '20
from typing import Tuple

import numpy as np
from .nmtf_utils import EPSILON, sparse_opt
import logging

logger = logging.getLogger(__name__)

# TODO (pcotte): typing
# TODO (pcotte): docstrings (with parameters and returns)


def ntf_stack(m, mmis, n_blocks):
    """Unfold tensor M
    for future use with NMF
    """
    n, p = m.shape
    mmis = mmis.astype(np.int)
    n_mmis = mmis.shape[0]
    n_blocks = int(n_blocks)

    mstacked = np.zeros((int(n * p / n_blocks), n_blocks))
    if n_mmis > 0:
        mmis_stacked = np.zeros((int(n * p / n_blocks), n_blocks))
    else:
        mmis_stacked = np.array([])

    for i_block in range(0, n_blocks):
        for j in range(0, int(p / n_blocks)):
            i1 = j * n
            i2 = i1 + n
            mstacked[i1:i2, i_block] = m[:, int(i_block * p / n_blocks + j)]
            if n_mmis > 0:
                mmis_stacked[i1:i2, i_block] = mmis[:, int(i_block * p / n_blocks + j)]

    return mstacked, mmis_stacked


def ntf_solve(
    m,
    mmis,
    mt0,
    mw0,
    mb0,
    nc,
    tolerance,
    log_iter,
    status0,
    max_iterations,
    nmf_fix_user_lhe,
    nmf_fix_user_rhe,
    nmf_fix_user_bhe,
    nmf_sparse_level,
    ntf_unimodal,
    ntf_smooth,
    ntf_left_components,
    ntf_right_components,
    ntf_block_components,
    n_blocks,
    nmf_priors,
    my_status_box,
):
    """Interface to:
    - NTFSolve_simple
    """

    if len(nmf_priors) > 0:
        n_nmf_priors, nc = nmf_priors.shape
    else:
        n_nmf_priors = 0

    if n_nmf_priors > 0:
        nmf_priors[nmf_priors > 0] = 1

    return ntf_solve_simple(
        m=m,
        mmis=mmis,
        mt0=mt0,
        mw0=mw0,
        mb0=mb0,
        nc=nc,
        tolerance=tolerance,
        log_iter=log_iter,
        status0=status0,
        max_iterations=max_iterations,
        nmf_fix_user_lhe=nmf_fix_user_lhe,
        nmf_fix_user_rhe=nmf_fix_user_rhe,
        nmf_fix_user_bhe=nmf_fix_user_bhe,
        nmf_sparse_level=nmf_sparse_level,
        ntf_unimodal=ntf_unimodal,
        ntf_smooth=ntf_smooth,
        ntf_left_components=ntf_left_components,
        ntf_right_components=ntf_right_components,
        ntf_block_components=ntf_block_components,
        n_blocks=n_blocks,
        nmf_priors=nmf_priors,
        my_status_box=my_status_box,
    )


def ntf_solve_simple(
    m,
    mmis,
    mt0,
    mw0,
    mb0,
    nc,
    tolerance,
    log_iter,
    status0,
    max_iterations,
    nmf_fix_user_lhe,
    nmf_fix_user_rhe,
    nmf_fix_user_bhe,
    nmf_sparse_level,
    ntf_unimodal,
    ntf_smooth,
    ntf_left_components,
    ntf_right_components,
    ntf_block_components,
    n_blocks,
    nmf_priors,
    my_status_box,
) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, float, int]:
    """
    Estimate NTF matrices (HALS)

    Parameters
    ----------
     m: Input matrix
     mmis: Define missing values (0 = missing cell, 1 = real cell)
     mt0: Initial left hand matrix
     mw0: Initial right hand matrix
     mb0: Initial block hand matrix
     nc: NTF rank
     tolerance: Convergence threshold
     log_iter: Log results through iterations
     status0: Initial displayed status to be updated during iterations
     max_iterations: Max iterations
     nmf_fix_user_lhe: = 1 => fixed left hand matrix columns
     nmf_fix_user_rhe: = 1 => fixed  right hand matrix columns
     nmf_fix_user_bhe: = 1 => fixed  block hand matrix columns
     nmf_sparse_level: sparsity level (as defined by Hoyer); +/- = make RHE/LHe sparse
     ntf_unimodal: Apply Unimodal constraint on factoring vectors
     ntf_smooth: Apply Smooth constraint on factoring vectors
     ntf_left_components: Apply Unimodal/Smooth constraint on left hand matrix
     ntf_right_components: Apply Unimodal/Smooth constraint on right hand matrix
     ntf_block_components: Apply Unimodal/Smooth constraint on block hand matrix
     n_blocks: Number of NTF blocks
     nmf_priors: Elements in mw that should be updated (others remain 0)
     my_status_box

    Returns
    -------
    Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, float, int]\n
       * mt: Left hand matrix\n
       * mw: Right hand matrix\n
       * mb: Block hand matrix\n
       * diff: objective cost\n
       * cancel_pressed\n

    Reference
    ---------
    a. Cichocki, P.H.a.N. Anh-Huym, Fast local algorithms for large scale nonnegative matrix and tensor factorizations,
    IEICE Trans. Fundam. Electron. Commun. Comput. Sci. 92 (3) (2009) 708–721.
    """

    cancel_pressed = 0

    n, p0 = m.shape
    n_mmis = mmis.shape[0]
    nc = int(nc)
    n_blocks = int(n_blocks)
    p = int(p0 / n_blocks)
    nxp = int(n * p)
    nxp0 = int(n * p0)
    mt = np.copy(mt0)
    mw = np.copy(mw0)
    mb = np.copy(mb0)
    #     step_iter = math.ceil(MaxIterations/10)
    step_iter = 1
    pbar_step = 100 * step_iter / max_iterations

    id_blockp = np.arange(0, (n_blocks - 1) * p + 1, p)
    a = np.zeros(n)
    b = np.zeros(p)
    c = np.zeros(n_blocks)
    alpha = np.zeros(nc)

    # Compute Residual tensor
    mfit = np.zeros((n, p0))
    for k in range(0, nc):
        if n_blocks > 1:
            for i_block in range(0, n_blocks):
                mfit[:, id_blockp[i_block]: id_blockp[i_block] + p] += (
                    mb[i_block, k] * np.reshape(mt[:, k], (n, 1)) @ np.reshape(mw[:, k], (1, p))
                )
        else:
            mfit[:, id_blockp[0]: id_blockp[0] + p] += np.reshape(mt[:, k], (n, 1)) @ np.reshape(mw[:, k], (1, p))

    denomt = np.zeros(n)
    denomw = np.zeros(p)
    denom_block = np.zeros((n_blocks, nc))
    mt2 = np.zeros(n)
    mw2 = np.zeros(p)
    mt_mw = np.zeros(nxp)
    denom_cutoff = 0.1

    if n_mmis > 0:
        mres = (m - mfit) * mmis
    else:
        mres = m - mfit

    my_status_box.init_bar()

    # Loop
    cont = 1
    i_iter = 0
    diff0 = 1.0e99
    mpart = np.zeros((n, p0))
    if abs(nmf_sparse_level) < 1:
        alpha[0] = nmf_sparse_level * 0.8
    else:
        alpha[0] = nmf_sparse_level

    percent_zeros = 0
    iter_sparse = 0

    while (cont > 0) & (i_iter < max_iterations):
        for k in range(0, nc):
            (
                n_blocks,
                mpart,
                id_blockp,
                p,
                mb,
                k,
                mt,
                n,
                mw,
                n_mmis,
                mmis,
                mres,
                nmf_fix_user_lhe,
                denomt,
                mw2,
                denom_cutoff,
                alpha,
                ntf_unimodal,
                ntf_left_components,
                ntf_smooth,
                a,
                nmf_fix_user_rhe,
                denomw,
                mt2,
                ntf_right_components,
                b,
                nmf_fix_user_bhe,
                mt_mw,
                nxp,
                denom_block,
                ntf_block_components,
                c,
                mfit,
                nmf_priors,
            ) = ntf_update(
                n_blocks=n_blocks,
                mpart=mpart,
                id_blockp=id_blockp,
                p=p,
                mb=mb,
                k=k,
                mt=mt,
                n=n,
                mw=mw,
                n_mmis=n_mmis,
                mmis=mmis,
                mres=mres,
                nmf_fix_user_lhe=nmf_fix_user_lhe,
                denomt=denomt,
                mw2=mw2,
                denom_cutoff=denom_cutoff,
                alpha=alpha,
                ntf_unimodal=ntf_unimodal,
                ntf_left_components=ntf_left_components,
                ntf_smooth=ntf_smooth,
                a=a,
                nmf_fix_user_rhe=nmf_fix_user_rhe,
                denomw=denomw,
                mt2=mt2,
                ntf_right_components=ntf_right_components,
                b=b,
                nmf_fix_user_bhe=nmf_fix_user_bhe,
                mt_mw=mt_mw,
                nxp=nxp,
                denom_block=denom_block,
                ntf_block_components=ntf_block_components,
                c=c,
                mfit=mfit,
                nmf_priors=nmf_priors,
            )

        if i_iter % step_iter == 0:
            # Check convergence
            diff = np.linalg.norm(mres) ** 2 / nxp0
            if (diff0 - diff) / diff0 < tolerance:
                cont = 0
            else:
                if diff > diff0:
                    my_status_box.my_print(f"{status0} Iter: {i_iter} MSR does not improve")

                diff0 = diff

            Status = f"{status0} Iteration: {i_iter}"

            if nmf_sparse_level != 0:
                Status = f"{Status} ; Achieved sparsity: {round(percent_zeros, 2)}; alpha: {round(alpha[0], 2)}"
                if log_iter == 1:
                    my_status_box.my_print(Status)

            my_status_box.update_status(status=Status)
            my_status_box.update_bar(step=pbar_step)
            if my_status_box.cancel_pressed:
                cancel_pressed = 1
                return np.array([]), mt, mw, mb, mres, cancel_pressed

            if log_iter == 1:
                my_status_box.my_print(status0 + " Iter: " + str(i_iter) + " MSR: " + str(diff))

        i_iter += 1

        if cont == 0 or i_iter == max_iterations or (cont == 0 and abs(nmf_sparse_level) == 1):
            if 0 < nmf_sparse_level < 1:
                sparse_test = np.zeros((nc, 1))
                percent_zeros0 = percent_zeros
                for k in range(0, nc):
                    sparse_test[k] = np.where(mw[:, k] == 0)[0].size

                percent_zeros = np.mean(sparse_test) / p
                if percent_zeros < percent_zeros0:
                    iter_sparse += 1
                else:
                    iter_sparse = 0

                if (percent_zeros < 0.99 * nmf_sparse_level) & (iter_sparse < 50):
                    alpha[0] *= min(1.05 * nmf_sparse_level / percent_zeros, 1.1)
                    if alpha[0] < 1:
                        i_iter = 0
                        cont = 1

            elif 0 > nmf_sparse_level > -1:
                sparse_test = np.zeros((nc, 1))
                percent_zeros0 = percent_zeros
                for k in range(0, nc):
                    sparse_test[k] = np.where(mt[:, k] == 0)[0].size

                percent_zeros = np.mean(sparse_test) / n
                if percent_zeros < percent_zeros0:
                    iter_sparse += 1
                else:
                    iter_sparse = 0

                if (percent_zeros < 0.99 * abs(nmf_sparse_level)) & (iter_sparse < 50):
                    alpha[0] *= min(1.05 * abs(nmf_sparse_level) / percent_zeros, 1.1)
                    if abs(alpha[0]) < 1:
                        i_iter = 0
                        cont = 1

            elif abs(alpha[0]) == 1:
                if alpha[0] == -1:
                    for k in range(0, nc):
                        if np.max(mt[:, k]) > 0:
                            hhi = int(
                                np.round(
                                    (np.linalg.norm(mt[:, k], ord=1) / (np.linalg.norm(mt[:, k], ord=2) + EPSILON))
                                    ** 2,
                                    decimals=0,
                                )
                            )
                            alpha[k] = -1 - (n - hhi) / (n - 1)
                        else:
                            alpha[k] = 0
                else:
                    for k in range(0, nc):
                        if np.max(mw[:, k]) > 0:
                            hhi = int(
                                np.round(
                                    (np.linalg.norm(mw[:, k], ord=1) / (np.linalg.norm(mw[:, k], ord=2) + EPSILON))
                                    ** 2,
                                    decimals=0,
                                )
                            )
                            alpha[k] = 1 + (p - hhi) / (p - 1)
                        else:
                            alpha[k] = 0

                if alpha[0] <= -1:
                    alpha_real = -(alpha + 1)
                    # noinspection PyTypeChecker
                    alpha_min = min(alpha_real)
                    for k in range(0, nc):
                        # noinspection PyUnresolvedReferences
                        alpha[k] = min(alpha_real[k], 2 * alpha_min)
                        alpha[k] = -alpha[k] - 1
                else:
                    alpha_real = alpha - 1
                    alpha_min = min(alpha_real)
                    for k in range(0, nc):
                        alpha[k] = min(alpha_real[k], 2 * alpha_min)
                        alpha[k] = alpha[k] + 1

                i_iter = 0
                cont = 1
                diff0 = 1.0e99

    for k in range(0, nc):
        if np.max(mt[:, k]) > 0:
            hhi = np.round((np.linalg.norm(mt[:, k], ord=1) / np.linalg.norm(mt[:, k], ord=2)) ** 2, decimals=0)
            logger.info(f"component: {k}, left hhi: {hhi}")

        if np.max(mw[:, k]) > 0:
            hhi = np.round((np.linalg.norm(mw[:, k], ord=1) / np.linalg.norm(mw[:, k], ord=2)) ** 2, decimals=0)
            logger.info(f"component: {k} right hhi: {hhi}")

    if (n_mmis > 0) & (nmf_fix_user_bhe == 0):
        mb *= denom_block

    # TODO (pcotte): mt and mw can be not yet referenced: fix that
    return np.array([]), mt, mw, mb, diff, cancel_pressed


def ntf_update(
    n_blocks,
    mpart,
    id_blockp,
    p,
    mb,
    k,
    mt,
    n,
    mw,
    n_mmis,
    mmis,
    mres,
    nmf_fix_user_lhe,
    denomt,
    mw2,
    denom_cutoff,
    alpha,
    ntf_unimodal,
    ntf_left_components,
    ntf_smooth,
    a,
    nmf_fix_user_rhe,
    denomw,
    mt2,
    ntf_right_components,
    b,
    nmf_fix_user_bhe,
    mt_mw,
    nxp,
    denom_block,
    ntf_block_components,
    c,
    mfit,
    nmf_priors,
):
    """Core updating code called by NTFSolve_simple & NTF Solve_conv
    Input:
        All variables in the calling function used in the function
    Output:
        Same as Input
    """

    if len(nmf_priors) > 0:
        n_nmf_priors, nc = nmf_priors.shape
    else:
        n_nmf_priors = 0

    # Compute kth-part
    if n_blocks > 1:
        for i_block in range(0, n_blocks):
            mpart[:, id_blockp[i_block]: id_blockp[i_block] + p] = (
                mb[i_block, k] * np.reshape(mt[:, k], (n, 1)) @ np.reshape(mw[:, k], (1, p))
            )
    else:
        mpart[:, id_blockp[0]: id_blockp[0] + p] = np.reshape(mt[:, k], (n, 1)) @ np.reshape(mw[:, k], (1, p))

    if n_mmis > 0:
        mpart *= mmis

    mpart += mres

    if nmf_fix_user_bhe > 0:
        norm_bhe = True
        if nmf_fix_user_rhe == 0:
            norm_lhe = True
            norm_rhe = False
        else:
            norm_lhe = False
            norm_rhe = True
    else:
        norm_bhe = False
        norm_lhe = True
        norm_rhe = True

    if (nmf_fix_user_lhe > 0) & norm_lhe:
        norm = np.linalg.norm(mt[:, k])
        if norm > 0:
            mt[:, k] /= norm

    if (nmf_fix_user_rhe > 0) & norm_rhe:
        norm = np.linalg.norm(mw[:, k])
        if norm > 0:
            mw[:, k] /= norm

    if (nmf_fix_user_bhe > 0) & norm_bhe & (n_blocks > 1):
        norm = np.linalg.norm(mb[:, k])
        if norm > 0:
            mb[:, k] /= norm

    if nmf_fix_user_lhe == 0:
        # Update Mt
        mt[:, k] = 0
        if n_blocks > 1:
            for i_block in range(0, n_blocks):
                mt[:, k] += mb[i_block, k] * mpart[:, id_blockp[i_block]: id_blockp[i_block] + p] @ mw[:, k]
        else:
            mt[:, k] += mpart[:, id_blockp[0]: id_blockp[0] + p] @ mw[:, k]

        if n_mmis > 0:
            denomt[:] = 0
            mw2[:] = mw[:, k] ** 2
            if n_blocks > 1:
                for i_block in range(0, n_blocks):
                    # Broadcast missing cells into Mw to calculate Mw.T * Mw
                    denomt += mb[i_block, k] ** 2 * mmis[:, id_blockp[i_block]: id_blockp[i_block] + p] @ mw2
            else:
                denomt += mmis[:, id_blockp[0]: id_blockp[0] + p] @ mw2

            denomt /= np.max(denomt)
            denomt[denomt < denom_cutoff] = denom_cutoff
            mt[:, k] /= denomt

        mt[mt[:, k] < 0, k] = 0
        if alpha[0] < 0:
            if alpha[0] <= -1:
                if (alpha[0] == -1) & (np.max(mt[:, k]) > 0):
                    t_threshold = mt[:, k]
                    hhi = int(
                        np.round(
                            (np.linalg.norm(t_threshold, ord=1) / (np.linalg.norm(t_threshold, ord=2) + EPSILON)) ** 2,
                            decimals=0,
                        )
                    )
                    t_rank = np.argsort(t_threshold)
                    t_threshold[t_rank[0: n - hhi]] = 0
                else:
                    mt[:, k] = sparse_opt(mt[:, k], -alpha[k] - 1, False)
            else:
                mt[:, k] = sparse_opt(mt[:, k], -alpha[0], False)

        if (ntf_unimodal > 0) & (ntf_left_components > 0):
            #             Enforce unimodal distribution
            tmax = np.argmax(mt[:, k])
            for i in range(tmax + 1, n):
                mt[i, k] = min(mt[i - 1, k], mt[i, k])

            for i in range(tmax - 1, -1, -1):
                mt[i, k] = min(mt[i + 1, k], mt[i, k])

        if (ntf_smooth > 0) & (ntf_left_components > 0):
            #             Smooth distribution
            a[0] = 0.75 * mt[0, k] + 0.25 * mt[1, k]
            a[n - 1] = 0.25 * mt[n - 2, k] + 0.75 * mt[n - 1, k]
            for i in range(1, n - 1):
                a[i] = 0.25 * mt[i - 1, k] + 0.5 * mt[i, k] + 0.25 * mt[i + 1, k]

            mt[:, k] = a

        if norm_lhe:
            norm = np.linalg.norm(mt[:, k])
            if norm > 0:
                mt[:, k] /= norm

    if nmf_fix_user_rhe == 0:
        # Update Mw
        mw[:, k] = 0
        if n_blocks > 1:
            for i_block in range(0, n_blocks):
                mw[:, k] += mpart[:, id_blockp[i_block]: id_blockp[i_block] + p].T @ mt[:, k] * mb[i_block, k]
        else:
            mw[:, k] += mpart[:, id_blockp[0]: id_blockp[0] + p].T @ mt[:, k]

        if n_mmis > 0:
            denomw[:] = 0
            mt2[:] = mt[:, k] ** 2
            if n_blocks > 1:
                for i_block in range(0, n_blocks):
                    # Broadcast missing cells into Mw to calculate Mt.T * Mt
                    denomw += mb[i_block, k] ** 2 * mmis[:, id_blockp[i_block]: id_blockp[i_block] + p].T @ mt2
            else:
                denomw += mmis[:, id_blockp[0]: id_blockp[0] + p].T @ mt2

            denomw /= np.max(denomw)
            denomw[denomw < denom_cutoff] = denom_cutoff
            mw[:, k] /= denomw

        mw[mw[:, k] < 0, k] = 0

        if alpha[0] > 0:
            if alpha[0] >= 1:
                if (alpha[0] == 1) & (np.max(mw[:, k]) > 0):
                    w_threshold = mw[:, k]
                    hhi = int(
                        np.round(
                            (np.linalg.norm(w_threshold, ord=1) / (np.linalg.norm(w_threshold, ord=2) + EPSILON)) ** 2,
                            decimals=0,
                        )
                    )
                    w_rank = np.argsort(w_threshold)
                    w_threshold[w_rank[0: p - hhi]] = 0
                else:
                    mw[:, k] = sparse_opt(mw[:, k], alpha[k] - 1, False)
            else:
                mw[:, k] = sparse_opt(mw[:, k], alpha[0], False)

        if (ntf_unimodal > 0) & (ntf_right_components > 0):
            # Enforce unimodal distribution
            wmax = np.argmax(mw[:, k])
            for j in range(wmax + 1, p):
                mw[j, k] = min(mw[j - 1, k], mw[j, k])

            for j in range(wmax - 1, -1, -1):
                mw[j, k] = min(mw[j + 1, k], mw[j, k])

        if (ntf_smooth > 0) & (ntf_right_components > 0):
            #             Smooth distribution
            b[0] = 0.75 * mw[0, k] + 0.25 * mw[1, k]
            b[p - 1] = 0.25 * mw[p - 2, k] + 0.75 * mw[p - 1, k]
            for j in range(1, p - 1):
                b[j] = 0.25 * mw[j - 1, k] + 0.5 * mw[j, k] + 0.25 * mw[j + 1, k]

            mw[:, k] = b

        if n_nmf_priors > 0:
            mw[:, k] = mw[:, k] * nmf_priors[:, k]

        if norm_rhe:
            norm = np.linalg.norm(mw[:, k])
            if norm > 0:
                mw[:, k] /= norm

    if nmf_fix_user_bhe == 0:
        # Update Mb
        mb[:, k] = 0
        mt_mw[:] = np.reshape((np.reshape(mt[:, k], (n, 1)) @ np.reshape(mw[:, k], (1, p))), nxp)

        for i_block in range(0, n_blocks):
            mb[i_block, k] = np.reshape(mpart[:, id_blockp[i_block]: id_blockp[i_block] + p], nxp).T @ mt_mw

        if n_mmis > 0:
            mt_mw[:] = mt_mw[:] ** 2
            for i_block in range(0, n_blocks):
                # Broadcast missing cells into Mb to calculate Mb.T * Mb
                denom_block[i_block, k] = (
                    np.reshape(mmis[:, id_blockp[i_block]: id_blockp[i_block] + p], (1, nxp)) @ mt_mw
                )

            maxdenom_block = np.max(denom_block[:, k])
            denom_block[denom_block[:, k] < denom_cutoff * maxdenom_block] = denom_cutoff * maxdenom_block
            mb[:, k] /= denom_block[:, k]

        mb[mb[:, k] < 0, k] = 0

        if (ntf_unimodal > 0) & (ntf_block_components > 0):
            #                 Enforce unimodal distribution
            bmax = np.argmax(mb[:, k])
            for i_block in range(bmax + 1, n_blocks):
                mb[i_block, k] = min(mb[i_block - 1, k], mb[i_block, k])

            for i_block in range(bmax - 1, -1, -1):
                mb[i_block, k] = min(mb[i_block + 1, k], mb[i_block, k])

        if (ntf_smooth > 0) & (ntf_block_components > 0):
            #             Smooth distribution
            c[0] = 0.75 * mb[0, k] + 0.25 * mb[1, k]
            c[n_blocks - 1] = 0.25 * mb[n_blocks - 2, k] + 0.75 * mb[n_blocks - 1, k]
            for i_block in range(1, n_blocks - 1):
                c[i_block] = 0.25 * mb[i_block - 1, k] + 0.5 * mb[i_block, k] + 0.25 * mb[i_block + 1, k]

            mb[:, k] = c

        if norm_bhe:
            norm = np.linalg.norm(mb[:, k])
            if norm > 0:
                mb[:, k] /= norm

    # Update residual tensor
    mfit[:, :] = 0
    if n_blocks > 1:
        for i_block in range(0, n_blocks):
            mfit[:, id_blockp[i_block]: id_blockp[i_block] + p] += (
                mb[i_block, k] * np.reshape(mt[:, k], (n, 1)) @ np.reshape(mw[:, k], (1, p))
            )
    else:
        mfit[:, id_blockp[0]: id_blockp[0] + p] += np.reshape(mt[:, k], (n, 1)) @ np.reshape(mw[:, k], (1, p))

    if n_mmis > 0:
        mres[:, :] = (mpart - mfit) * mmis
    else:
        mres[:, :] = mpart - mfit

    return (
        n_blocks,
        mpart,
        id_blockp,
        p,
        mb,
        k,
        mt,
        n,
        mw,
        n_mmis,
        mmis,
        mres,
        nmf_fix_user_lhe,
        denomt,
        mw2,
        denom_cutoff,
        alpha,
        ntf_unimodal,
        ntf_left_components,
        ntf_smooth,
        a,
        nmf_fix_user_rhe,
        denomw,
        mt2,
        ntf_right_components,
        b,
        nmf_fix_user_bhe,
        mt_mw,
        nxp,
        denom_block,
        ntf_block_components,
        c,
        mfit,
        nmf_priors,
    )

Functions

def ntf_solve(m, mmis, mt0, mw0, mb0, nc, tolerance, log_iter, status0, max_iterations, nmf_fix_user_lhe, nmf_fix_user_rhe, nmf_fix_user_bhe, nmf_sparse_level, ntf_unimodal, ntf_smooth, ntf_left_components, ntf_right_components, ntf_block_components, n_blocks, nmf_priors, my_status_box)

Interface to: - NTFSolve_simple

Expand source code
def ntf_solve(
    m,
    mmis,
    mt0,
    mw0,
    mb0,
    nc,
    tolerance,
    log_iter,
    status0,
    max_iterations,
    nmf_fix_user_lhe,
    nmf_fix_user_rhe,
    nmf_fix_user_bhe,
    nmf_sparse_level,
    ntf_unimodal,
    ntf_smooth,
    ntf_left_components,
    ntf_right_components,
    ntf_block_components,
    n_blocks,
    nmf_priors,
    my_status_box,
):
    """Interface to:
    - NTFSolve_simple
    """

    if len(nmf_priors) > 0:
        n_nmf_priors, nc = nmf_priors.shape
    else:
        n_nmf_priors = 0

    if n_nmf_priors > 0:
        nmf_priors[nmf_priors > 0] = 1

    return ntf_solve_simple(
        m=m,
        mmis=mmis,
        mt0=mt0,
        mw0=mw0,
        mb0=mb0,
        nc=nc,
        tolerance=tolerance,
        log_iter=log_iter,
        status0=status0,
        max_iterations=max_iterations,
        nmf_fix_user_lhe=nmf_fix_user_lhe,
        nmf_fix_user_rhe=nmf_fix_user_rhe,
        nmf_fix_user_bhe=nmf_fix_user_bhe,
        nmf_sparse_level=nmf_sparse_level,
        ntf_unimodal=ntf_unimodal,
        ntf_smooth=ntf_smooth,
        ntf_left_components=ntf_left_components,
        ntf_right_components=ntf_right_components,
        ntf_block_components=ntf_block_components,
        n_blocks=n_blocks,
        nmf_priors=nmf_priors,
        my_status_box=my_status_box,
    )
def ntf_solve_simple(m, mmis, mt0, mw0, mb0, nc, tolerance, log_iter, status0, max_iterations, nmf_fix_user_lhe, nmf_fix_user_rhe, nmf_fix_user_bhe, nmf_sparse_level, ntf_unimodal, ntf_smooth, ntf_left_components, ntf_right_components, ntf_block_components, n_blocks, nmf_priors, my_status_box) ‑> Tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray, float, int]

Estimate NTF matrices (HALS)

Parameters

m: Input matrix mmis: Define missing values (0 = missing cell, 1 = real cell) mt0: Initial left hand matrix mw0: Initial right hand matrix mb0: Initial block hand matrix nc: NTF rank tolerance: Convergence threshold log_iter: Log results through iterations status0: Initial displayed status to be updated during iterations max_iterations: Max iterations nmf_fix_user_lhe: = 1 => fixed left hand matrix columns nmf_fix_user_rhe: = 1 => fixed right hand matrix columns nmf_fix_user_bhe: = 1 => fixed block hand matrix columns nmf_sparse_level: sparsity level (as defined by Hoyer); +/- = make RHE/LHe sparse ntf_unimodal: Apply Unimodal constraint on factoring vectors ntf_smooth: Apply Smooth constraint on factoring vectors ntf_left_components: Apply Unimodal/Smooth constraint on left hand matrix ntf_right_components: Apply Unimodal/Smooth constraint on right hand matrix ntf_block_components: Apply Unimodal/Smooth constraint on block hand matrix n_blocks: Number of NTF blocks nmf_priors: Elements in mw that should be updated (others remain 0) my_status_box

Returns

Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, float, int]
 
  • mt: Left hand matrix

  • mw: Right hand matrix

  • mb: Block hand matrix

  • diff: objective cost

  • cancel_pressed

Reference

a. Cichocki, P.H.a.N. Anh-Huym, Fast local algorithms for large scale nonnegative matrix and tensor factorizations, IEICE Trans. Fundam. Electron. Commun. Comput. Sci. 92 (3) (2009) 708–721.

Expand source code
def ntf_solve_simple(
    m,
    mmis,
    mt0,
    mw0,
    mb0,
    nc,
    tolerance,
    log_iter,
    status0,
    max_iterations,
    nmf_fix_user_lhe,
    nmf_fix_user_rhe,
    nmf_fix_user_bhe,
    nmf_sparse_level,
    ntf_unimodal,
    ntf_smooth,
    ntf_left_components,
    ntf_right_components,
    ntf_block_components,
    n_blocks,
    nmf_priors,
    my_status_box,
) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, float, int]:
    """
    Estimate NTF matrices (HALS)

    Parameters
    ----------
     m: Input matrix
     mmis: Define missing values (0 = missing cell, 1 = real cell)
     mt0: Initial left hand matrix
     mw0: Initial right hand matrix
     mb0: Initial block hand matrix
     nc: NTF rank
     tolerance: Convergence threshold
     log_iter: Log results through iterations
     status0: Initial displayed status to be updated during iterations
     max_iterations: Max iterations
     nmf_fix_user_lhe: = 1 => fixed left hand matrix columns
     nmf_fix_user_rhe: = 1 => fixed  right hand matrix columns
     nmf_fix_user_bhe: = 1 => fixed  block hand matrix columns
     nmf_sparse_level: sparsity level (as defined by Hoyer); +/- = make RHE/LHe sparse
     ntf_unimodal: Apply Unimodal constraint on factoring vectors
     ntf_smooth: Apply Smooth constraint on factoring vectors
     ntf_left_components: Apply Unimodal/Smooth constraint on left hand matrix
     ntf_right_components: Apply Unimodal/Smooth constraint on right hand matrix
     ntf_block_components: Apply Unimodal/Smooth constraint on block hand matrix
     n_blocks: Number of NTF blocks
     nmf_priors: Elements in mw that should be updated (others remain 0)
     my_status_box

    Returns
    -------
    Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, float, int]\n
       * mt: Left hand matrix\n
       * mw: Right hand matrix\n
       * mb: Block hand matrix\n
       * diff: objective cost\n
       * cancel_pressed\n

    Reference
    ---------
    a. Cichocki, P.H.a.N. Anh-Huym, Fast local algorithms for large scale nonnegative matrix and tensor factorizations,
    IEICE Trans. Fundam. Electron. Commun. Comput. Sci. 92 (3) (2009) 708–721.
    """

    cancel_pressed = 0

    n, p0 = m.shape
    n_mmis = mmis.shape[0]
    nc = int(nc)
    n_blocks = int(n_blocks)
    p = int(p0 / n_blocks)
    nxp = int(n * p)
    nxp0 = int(n * p0)
    mt = np.copy(mt0)
    mw = np.copy(mw0)
    mb = np.copy(mb0)
    #     step_iter = math.ceil(MaxIterations/10)
    step_iter = 1
    pbar_step = 100 * step_iter / max_iterations

    id_blockp = np.arange(0, (n_blocks - 1) * p + 1, p)
    a = np.zeros(n)
    b = np.zeros(p)
    c = np.zeros(n_blocks)
    alpha = np.zeros(nc)

    # Compute Residual tensor
    mfit = np.zeros((n, p0))
    for k in range(0, nc):
        if n_blocks > 1:
            for i_block in range(0, n_blocks):
                mfit[:, id_blockp[i_block]: id_blockp[i_block] + p] += (
                    mb[i_block, k] * np.reshape(mt[:, k], (n, 1)) @ np.reshape(mw[:, k], (1, p))
                )
        else:
            mfit[:, id_blockp[0]: id_blockp[0] + p] += np.reshape(mt[:, k], (n, 1)) @ np.reshape(mw[:, k], (1, p))

    denomt = np.zeros(n)
    denomw = np.zeros(p)
    denom_block = np.zeros((n_blocks, nc))
    mt2 = np.zeros(n)
    mw2 = np.zeros(p)
    mt_mw = np.zeros(nxp)
    denom_cutoff = 0.1

    if n_mmis > 0:
        mres = (m - mfit) * mmis
    else:
        mres = m - mfit

    my_status_box.init_bar()

    # Loop
    cont = 1
    i_iter = 0
    diff0 = 1.0e99
    mpart = np.zeros((n, p0))
    if abs(nmf_sparse_level) < 1:
        alpha[0] = nmf_sparse_level * 0.8
    else:
        alpha[0] = nmf_sparse_level

    percent_zeros = 0
    iter_sparse = 0

    while (cont > 0) & (i_iter < max_iterations):
        for k in range(0, nc):
            (
                n_blocks,
                mpart,
                id_blockp,
                p,
                mb,
                k,
                mt,
                n,
                mw,
                n_mmis,
                mmis,
                mres,
                nmf_fix_user_lhe,
                denomt,
                mw2,
                denom_cutoff,
                alpha,
                ntf_unimodal,
                ntf_left_components,
                ntf_smooth,
                a,
                nmf_fix_user_rhe,
                denomw,
                mt2,
                ntf_right_components,
                b,
                nmf_fix_user_bhe,
                mt_mw,
                nxp,
                denom_block,
                ntf_block_components,
                c,
                mfit,
                nmf_priors,
            ) = ntf_update(
                n_blocks=n_blocks,
                mpart=mpart,
                id_blockp=id_blockp,
                p=p,
                mb=mb,
                k=k,
                mt=mt,
                n=n,
                mw=mw,
                n_mmis=n_mmis,
                mmis=mmis,
                mres=mres,
                nmf_fix_user_lhe=nmf_fix_user_lhe,
                denomt=denomt,
                mw2=mw2,
                denom_cutoff=denom_cutoff,
                alpha=alpha,
                ntf_unimodal=ntf_unimodal,
                ntf_left_components=ntf_left_components,
                ntf_smooth=ntf_smooth,
                a=a,
                nmf_fix_user_rhe=nmf_fix_user_rhe,
                denomw=denomw,
                mt2=mt2,
                ntf_right_components=ntf_right_components,
                b=b,
                nmf_fix_user_bhe=nmf_fix_user_bhe,
                mt_mw=mt_mw,
                nxp=nxp,
                denom_block=denom_block,
                ntf_block_components=ntf_block_components,
                c=c,
                mfit=mfit,
                nmf_priors=nmf_priors,
            )

        if i_iter % step_iter == 0:
            # Check convergence
            diff = np.linalg.norm(mres) ** 2 / nxp0
            if (diff0 - diff) / diff0 < tolerance:
                cont = 0
            else:
                if diff > diff0:
                    my_status_box.my_print(f"{status0} Iter: {i_iter} MSR does not improve")

                diff0 = diff

            Status = f"{status0} Iteration: {i_iter}"

            if nmf_sparse_level != 0:
                Status = f"{Status} ; Achieved sparsity: {round(percent_zeros, 2)}; alpha: {round(alpha[0], 2)}"
                if log_iter == 1:
                    my_status_box.my_print(Status)

            my_status_box.update_status(status=Status)
            my_status_box.update_bar(step=pbar_step)
            if my_status_box.cancel_pressed:
                cancel_pressed = 1
                return np.array([]), mt, mw, mb, mres, cancel_pressed

            if log_iter == 1:
                my_status_box.my_print(status0 + " Iter: " + str(i_iter) + " MSR: " + str(diff))

        i_iter += 1

        if cont == 0 or i_iter == max_iterations or (cont == 0 and abs(nmf_sparse_level) == 1):
            if 0 < nmf_sparse_level < 1:
                sparse_test = np.zeros((nc, 1))
                percent_zeros0 = percent_zeros
                for k in range(0, nc):
                    sparse_test[k] = np.where(mw[:, k] == 0)[0].size

                percent_zeros = np.mean(sparse_test) / p
                if percent_zeros < percent_zeros0:
                    iter_sparse += 1
                else:
                    iter_sparse = 0

                if (percent_zeros < 0.99 * nmf_sparse_level) & (iter_sparse < 50):
                    alpha[0] *= min(1.05 * nmf_sparse_level / percent_zeros, 1.1)
                    if alpha[0] < 1:
                        i_iter = 0
                        cont = 1

            elif 0 > nmf_sparse_level > -1:
                sparse_test = np.zeros((nc, 1))
                percent_zeros0 = percent_zeros
                for k in range(0, nc):
                    sparse_test[k] = np.where(mt[:, k] == 0)[0].size

                percent_zeros = np.mean(sparse_test) / n
                if percent_zeros < percent_zeros0:
                    iter_sparse += 1
                else:
                    iter_sparse = 0

                if (percent_zeros < 0.99 * abs(nmf_sparse_level)) & (iter_sparse < 50):
                    alpha[0] *= min(1.05 * abs(nmf_sparse_level) / percent_zeros, 1.1)
                    if abs(alpha[0]) < 1:
                        i_iter = 0
                        cont = 1

            elif abs(alpha[0]) == 1:
                if alpha[0] == -1:
                    for k in range(0, nc):
                        if np.max(mt[:, k]) > 0:
                            hhi = int(
                                np.round(
                                    (np.linalg.norm(mt[:, k], ord=1) / (np.linalg.norm(mt[:, k], ord=2) + EPSILON))
                                    ** 2,
                                    decimals=0,
                                )
                            )
                            alpha[k] = -1 - (n - hhi) / (n - 1)
                        else:
                            alpha[k] = 0
                else:
                    for k in range(0, nc):
                        if np.max(mw[:, k]) > 0:
                            hhi = int(
                                np.round(
                                    (np.linalg.norm(mw[:, k], ord=1) / (np.linalg.norm(mw[:, k], ord=2) + EPSILON))
                                    ** 2,
                                    decimals=0,
                                )
                            )
                            alpha[k] = 1 + (p - hhi) / (p - 1)
                        else:
                            alpha[k] = 0

                if alpha[0] <= -1:
                    alpha_real = -(alpha + 1)
                    # noinspection PyTypeChecker
                    alpha_min = min(alpha_real)
                    for k in range(0, nc):
                        # noinspection PyUnresolvedReferences
                        alpha[k] = min(alpha_real[k], 2 * alpha_min)
                        alpha[k] = -alpha[k] - 1
                else:
                    alpha_real = alpha - 1
                    alpha_min = min(alpha_real)
                    for k in range(0, nc):
                        alpha[k] = min(alpha_real[k], 2 * alpha_min)
                        alpha[k] = alpha[k] + 1

                i_iter = 0
                cont = 1
                diff0 = 1.0e99

    for k in range(0, nc):
        if np.max(mt[:, k]) > 0:
            hhi = np.round((np.linalg.norm(mt[:, k], ord=1) / np.linalg.norm(mt[:, k], ord=2)) ** 2, decimals=0)
            logger.info(f"component: {k}, left hhi: {hhi}")

        if np.max(mw[:, k]) > 0:
            hhi = np.round((np.linalg.norm(mw[:, k], ord=1) / np.linalg.norm(mw[:, k], ord=2)) ** 2, decimals=0)
            logger.info(f"component: {k} right hhi: {hhi}")

    if (n_mmis > 0) & (nmf_fix_user_bhe == 0):
        mb *= denom_block

    # TODO (pcotte): mt and mw can be not yet referenced: fix that
    return np.array([]), mt, mw, mb, diff, cancel_pressed
def ntf_stack(m, mmis, n_blocks)

Unfold tensor M for future use with NMF

Expand source code
def ntf_stack(m, mmis, n_blocks):
    """Unfold tensor M
    for future use with NMF
    """
    n, p = m.shape
    mmis = mmis.astype(np.int)
    n_mmis = mmis.shape[0]
    n_blocks = int(n_blocks)

    mstacked = np.zeros((int(n * p / n_blocks), n_blocks))
    if n_mmis > 0:
        mmis_stacked = np.zeros((int(n * p / n_blocks), n_blocks))
    else:
        mmis_stacked = np.array([])

    for i_block in range(0, n_blocks):
        for j in range(0, int(p / n_blocks)):
            i1 = j * n
            i2 = i1 + n
            mstacked[i1:i2, i_block] = m[:, int(i_block * p / n_blocks + j)]
            if n_mmis > 0:
                mmis_stacked[i1:i2, i_block] = mmis[:, int(i_block * p / n_blocks + j)]

    return mstacked, mmis_stacked
def ntf_update(n_blocks, mpart, id_blockp, p, mb, k, mt, n, mw, n_mmis, mmis, mres, nmf_fix_user_lhe, denomt, mw2, denom_cutoff, alpha, ntf_unimodal, ntf_left_components, ntf_smooth, a, nmf_fix_user_rhe, denomw, mt2, ntf_right_components, b, nmf_fix_user_bhe, mt_mw, nxp, denom_block, ntf_block_components, c, mfit, nmf_priors)

Core updating code called by NTFSolve_simple & NTF Solve_conv

Input

All variables in the calling function used in the function

Output

Same as Input

Expand source code
def ntf_update(
    n_blocks,
    mpart,
    id_blockp,
    p,
    mb,
    k,
    mt,
    n,
    mw,
    n_mmis,
    mmis,
    mres,
    nmf_fix_user_lhe,
    denomt,
    mw2,
    denom_cutoff,
    alpha,
    ntf_unimodal,
    ntf_left_components,
    ntf_smooth,
    a,
    nmf_fix_user_rhe,
    denomw,
    mt2,
    ntf_right_components,
    b,
    nmf_fix_user_bhe,
    mt_mw,
    nxp,
    denom_block,
    ntf_block_components,
    c,
    mfit,
    nmf_priors,
):
    """Core updating code called by NTFSolve_simple & NTF Solve_conv
    Input:
        All variables in the calling function used in the function
    Output:
        Same as Input
    """

    if len(nmf_priors) > 0:
        n_nmf_priors, nc = nmf_priors.shape
    else:
        n_nmf_priors = 0

    # Compute kth-part
    if n_blocks > 1:
        for i_block in range(0, n_blocks):
            mpart[:, id_blockp[i_block]: id_blockp[i_block] + p] = (
                mb[i_block, k] * np.reshape(mt[:, k], (n, 1)) @ np.reshape(mw[:, k], (1, p))
            )
    else:
        mpart[:, id_blockp[0]: id_blockp[0] + p] = np.reshape(mt[:, k], (n, 1)) @ np.reshape(mw[:, k], (1, p))

    if n_mmis > 0:
        mpart *= mmis

    mpart += mres

    if nmf_fix_user_bhe > 0:
        norm_bhe = True
        if nmf_fix_user_rhe == 0:
            norm_lhe = True
            norm_rhe = False
        else:
            norm_lhe = False
            norm_rhe = True
    else:
        norm_bhe = False
        norm_lhe = True
        norm_rhe = True

    if (nmf_fix_user_lhe > 0) & norm_lhe:
        norm = np.linalg.norm(mt[:, k])
        if norm > 0:
            mt[:, k] /= norm

    if (nmf_fix_user_rhe > 0) & norm_rhe:
        norm = np.linalg.norm(mw[:, k])
        if norm > 0:
            mw[:, k] /= norm

    if (nmf_fix_user_bhe > 0) & norm_bhe & (n_blocks > 1):
        norm = np.linalg.norm(mb[:, k])
        if norm > 0:
            mb[:, k] /= norm

    if nmf_fix_user_lhe == 0:
        # Update Mt
        mt[:, k] = 0
        if n_blocks > 1:
            for i_block in range(0, n_blocks):
                mt[:, k] += mb[i_block, k] * mpart[:, id_blockp[i_block]: id_blockp[i_block] + p] @ mw[:, k]
        else:
            mt[:, k] += mpart[:, id_blockp[0]: id_blockp[0] + p] @ mw[:, k]

        if n_mmis > 0:
            denomt[:] = 0
            mw2[:] = mw[:, k] ** 2
            if n_blocks > 1:
                for i_block in range(0, n_blocks):
                    # Broadcast missing cells into Mw to calculate Mw.T * Mw
                    denomt += mb[i_block, k] ** 2 * mmis[:, id_blockp[i_block]: id_blockp[i_block] + p] @ mw2
            else:
                denomt += mmis[:, id_blockp[0]: id_blockp[0] + p] @ mw2

            denomt /= np.max(denomt)
            denomt[denomt < denom_cutoff] = denom_cutoff
            mt[:, k] /= denomt

        mt[mt[:, k] < 0, k] = 0
        if alpha[0] < 0:
            if alpha[0] <= -1:
                if (alpha[0] == -1) & (np.max(mt[:, k]) > 0):
                    t_threshold = mt[:, k]
                    hhi = int(
                        np.round(
                            (np.linalg.norm(t_threshold, ord=1) / (np.linalg.norm(t_threshold, ord=2) + EPSILON)) ** 2,
                            decimals=0,
                        )
                    )
                    t_rank = np.argsort(t_threshold)
                    t_threshold[t_rank[0: n - hhi]] = 0
                else:
                    mt[:, k] = sparse_opt(mt[:, k], -alpha[k] - 1, False)
            else:
                mt[:, k] = sparse_opt(mt[:, k], -alpha[0], False)

        if (ntf_unimodal > 0) & (ntf_left_components > 0):
            #             Enforce unimodal distribution
            tmax = np.argmax(mt[:, k])
            for i in range(tmax + 1, n):
                mt[i, k] = min(mt[i - 1, k], mt[i, k])

            for i in range(tmax - 1, -1, -1):
                mt[i, k] = min(mt[i + 1, k], mt[i, k])

        if (ntf_smooth > 0) & (ntf_left_components > 0):
            #             Smooth distribution
            a[0] = 0.75 * mt[0, k] + 0.25 * mt[1, k]
            a[n - 1] = 0.25 * mt[n - 2, k] + 0.75 * mt[n - 1, k]
            for i in range(1, n - 1):
                a[i] = 0.25 * mt[i - 1, k] + 0.5 * mt[i, k] + 0.25 * mt[i + 1, k]

            mt[:, k] = a

        if norm_lhe:
            norm = np.linalg.norm(mt[:, k])
            if norm > 0:
                mt[:, k] /= norm

    if nmf_fix_user_rhe == 0:
        # Update Mw
        mw[:, k] = 0
        if n_blocks > 1:
            for i_block in range(0, n_blocks):
                mw[:, k] += mpart[:, id_blockp[i_block]: id_blockp[i_block] + p].T @ mt[:, k] * mb[i_block, k]
        else:
            mw[:, k] += mpart[:, id_blockp[0]: id_blockp[0] + p].T @ mt[:, k]

        if n_mmis > 0:
            denomw[:] = 0
            mt2[:] = mt[:, k] ** 2
            if n_blocks > 1:
                for i_block in range(0, n_blocks):
                    # Broadcast missing cells into Mw to calculate Mt.T * Mt
                    denomw += mb[i_block, k] ** 2 * mmis[:, id_blockp[i_block]: id_blockp[i_block] + p].T @ mt2
            else:
                denomw += mmis[:, id_blockp[0]: id_blockp[0] + p].T @ mt2

            denomw /= np.max(denomw)
            denomw[denomw < denom_cutoff] = denom_cutoff
            mw[:, k] /= denomw

        mw[mw[:, k] < 0, k] = 0

        if alpha[0] > 0:
            if alpha[0] >= 1:
                if (alpha[0] == 1) & (np.max(mw[:, k]) > 0):
                    w_threshold = mw[:, k]
                    hhi = int(
                        np.round(
                            (np.linalg.norm(w_threshold, ord=1) / (np.linalg.norm(w_threshold, ord=2) + EPSILON)) ** 2,
                            decimals=0,
                        )
                    )
                    w_rank = np.argsort(w_threshold)
                    w_threshold[w_rank[0: p - hhi]] = 0
                else:
                    mw[:, k] = sparse_opt(mw[:, k], alpha[k] - 1, False)
            else:
                mw[:, k] = sparse_opt(mw[:, k], alpha[0], False)

        if (ntf_unimodal > 0) & (ntf_right_components > 0):
            # Enforce unimodal distribution
            wmax = np.argmax(mw[:, k])
            for j in range(wmax + 1, p):
                mw[j, k] = min(mw[j - 1, k], mw[j, k])

            for j in range(wmax - 1, -1, -1):
                mw[j, k] = min(mw[j + 1, k], mw[j, k])

        if (ntf_smooth > 0) & (ntf_right_components > 0):
            #             Smooth distribution
            b[0] = 0.75 * mw[0, k] + 0.25 * mw[1, k]
            b[p - 1] = 0.25 * mw[p - 2, k] + 0.75 * mw[p - 1, k]
            for j in range(1, p - 1):
                b[j] = 0.25 * mw[j - 1, k] + 0.5 * mw[j, k] + 0.25 * mw[j + 1, k]

            mw[:, k] = b

        if n_nmf_priors > 0:
            mw[:, k] = mw[:, k] * nmf_priors[:, k]

        if norm_rhe:
            norm = np.linalg.norm(mw[:, k])
            if norm > 0:
                mw[:, k] /= norm

    if nmf_fix_user_bhe == 0:
        # Update Mb
        mb[:, k] = 0
        mt_mw[:] = np.reshape((np.reshape(mt[:, k], (n, 1)) @ np.reshape(mw[:, k], (1, p))), nxp)

        for i_block in range(0, n_blocks):
            mb[i_block, k] = np.reshape(mpart[:, id_blockp[i_block]: id_blockp[i_block] + p], nxp).T @ mt_mw

        if n_mmis > 0:
            mt_mw[:] = mt_mw[:] ** 2
            for i_block in range(0, n_blocks):
                # Broadcast missing cells into Mb to calculate Mb.T * Mb
                denom_block[i_block, k] = (
                    np.reshape(mmis[:, id_blockp[i_block]: id_blockp[i_block] + p], (1, nxp)) @ mt_mw
                )

            maxdenom_block = np.max(denom_block[:, k])
            denom_block[denom_block[:, k] < denom_cutoff * maxdenom_block] = denom_cutoff * maxdenom_block
            mb[:, k] /= denom_block[:, k]

        mb[mb[:, k] < 0, k] = 0

        if (ntf_unimodal > 0) & (ntf_block_components > 0):
            #                 Enforce unimodal distribution
            bmax = np.argmax(mb[:, k])
            for i_block in range(bmax + 1, n_blocks):
                mb[i_block, k] = min(mb[i_block - 1, k], mb[i_block, k])

            for i_block in range(bmax - 1, -1, -1):
                mb[i_block, k] = min(mb[i_block + 1, k], mb[i_block, k])

        if (ntf_smooth > 0) & (ntf_block_components > 0):
            #             Smooth distribution
            c[0] = 0.75 * mb[0, k] + 0.25 * mb[1, k]
            c[n_blocks - 1] = 0.25 * mb[n_blocks - 2, k] + 0.75 * mb[n_blocks - 1, k]
            for i_block in range(1, n_blocks - 1):
                c[i_block] = 0.25 * mb[i_block - 1, k] + 0.5 * mb[i_block, k] + 0.25 * mb[i_block + 1, k]

            mb[:, k] = c

        if norm_bhe:
            norm = np.linalg.norm(mb[:, k])
            if norm > 0:
                mb[:, k] /= norm

    # Update residual tensor
    mfit[:, :] = 0
    if n_blocks > 1:
        for i_block in range(0, n_blocks):
            mfit[:, id_blockp[i_block]: id_blockp[i_block] + p] += (
                mb[i_block, k] * np.reshape(mt[:, k], (n, 1)) @ np.reshape(mw[:, k], (1, p))
            )
    else:
        mfit[:, id_blockp[0]: id_blockp[0] + p] += np.reshape(mt[:, k], (n, 1)) @ np.reshape(mw[:, k], (1, p))

    if n_mmis > 0:
        mres[:, :] = (mpart - mfit) * mmis
    else:
        mres[:, :] = mpart - mfit

    return (
        n_blocks,
        mpart,
        id_blockp,
        p,
        mb,
        k,
        mt,
        n,
        mw,
        n_mmis,
        mmis,
        mres,
        nmf_fix_user_lhe,
        denomt,
        mw2,
        denom_cutoff,
        alpha,
        ntf_unimodal,
        ntf_left_components,
        ntf_smooth,
        a,
        nmf_fix_user_rhe,
        denomw,
        mt2,
        ntf_right_components,
        b,
        nmf_fix_user_bhe,
        mt_mw,
        nxp,
        denom_block,
        ntf_block_components,
        c,
        mfit,
        nmf_priors,
    )