I am implementi a RS , that works with logical rules.
So the following two functions implements the AND ( conjunction ) and the NOT ( DISJUNCTION) OPERATION but they work with values in (-1, 1).

def conjunction_rule(self, u: torch.Tensor, i: torch.Tensor) -> torch.Tensor:
    """
    Compute the conjunction (AND) function, extended to handle negative values.

    Args:
        u (torch.Tensor): Tensor of shape [batch_size, embed_k], representing user embeddings.
        i (torch.Tensor): Tensor of shape [batch_size, embed_k], representing item embeddings.
        epsilon (float, optional): Small constant to prevent log(0). Defaults to 1e-40.

    Returns:
        torch.Tensor: Tensor of shape [batch_size], containing the conjunction scores.
    """
    # Process variables to handle possible negations
    u_v, u_is_negated = self.process_variable(u)
    i_v, i_is_negated = self.process_variable(i)

    # Adjust variables based on negation
    u_v_adj = torch.where(u_is_negated, 1 - u_v, u_v)
    i_v_adj = torch.where(i_is_negated, 1 - i_v, i_v)

    # Compute the element-wise terms
    expr = 1 - u_v_adj * (1 - i_v_adj) + self.epsilon  # epsilon prevents log(0)

    # Take the logarithm of each term
    log_expr = torch.log(expr)

    # Sum over the embedding dimensions
    sum_log_expr = torch.sum(log_expr, dim=1)

    # Compute the final AND function value
    res = -1.0 / (-1.0 + sum_log_expr)
    return res

def disjunction_rule(self, and_rules: torch.Tensor) -> torch.Tensor:
    """
    Compute the disjunction (OR) function, extended to handle negative values.

    Args:
        and_rules (torch.Tensor): Tensor of shape [batch_size, n_rules], containing conjunction scores.
        epsilon (float, optional): Small constant to prevent log(0). Defaults to 1e-40.

    Returns:
        torch.Tensor: Tensor of shape [batch_size], containing the disjunction scores.
    """
    # Process variables to handle possible negations
    and_rules_v, and_rules_is_negated = self.process_variable(and_rules)

    # Adjust variables based on negation
    and_rules_v_adj = torch.where(and_rules_is_negated, 1 - and_rules_v, and_rules_v)

    # Compute the element-wise terms
    expr = 1 - and_rules_v_adj + self.epsilon  # epsilon prevents log(0)

    # Take the logarithm of each term
    log_expr = torch.log(expr)

    # Sum over the rules
    sum_log_expr = torch.sum(log_expr, dim=1)

    # Compute the final OR function value
    res = 1 - (-1.0 / (-1.0 + sum_log_expr))
    return res

The values in u tells what values in i to take into account in the and operation, for example:
i'll explain better with an example with results, considering one user (u1) with two rules, each rules is of dim=4 and more items:
u1 = [1, 1, 0, 0]
U2 = [0, 0, 1, 0]
U3 = [-1, 0, 1, 0]
i1 = [0.9, 1, 0.2, 0]
I2 =  [-0.9, 1, 1, 0]


U1-i1:
Will give as results  1 for the first u1-i1  i.e.: [1, 1, 0, 0] choose the first two embedding of i [0.9, 1, 0.2, 0],    i.e = 0.9 AND 1 = 1 (almost)

U1-i1: for the second user u2 = [0, 0, 1, 0],  will choose the third element of i1 [0.9, 1, 0.2, 0] , I.e. 0.2

U3-i2:
Will give a result of 1



So these two functions are use in a forward pass to calculate the score as follows:  def forward(self, inputs, **kwargs):
    users, items = inputs
    batch_size = users.shape[0]

    # Get user embeddings and reshape for rules
    gu = self.Gu.weight[users]
    gu = gu.view(batch_size, self.n_rules, self.embed_k)  # 3 dim: 1st_dim = user_id, 2nd_dim = rule_no, 3d_dim = embedding of user-rule
    # gu = torch.clamp(gu, 0.0, 1.0)  # Before division in != predicate Ensure embeddings are in [0,1]

    # Get item embeddings
    gamma_i = self.Gi.weight[items]  # Shape: [batch_size, embed_k]
    # gamma_i = torch.clamp(gamma_i, 0.0, 1.0)

    if self.predicate == 'AND':
        gu = torch.clamp(gu, 0.0, 1.0)
        gamma_i = torch.clamp(gamma_i, 0.0, 1.0)

    # Compute conjunctions for each rule
    scores = []
    for r in range(self.n_rules):
        gamma_u_r = gu[:, r, :]  # User embedding for rule r
        if self.predicate == 'AND':
            # gamma_i = torch.clamp(gamma_i, 0.0, 1.0)
            # score = self.conjunction_rule(u=torch.clamp(gamma_u_r, 0.0, 1.0), i=gamma_i)  # Shape: [batch_size]
            score = self.conjunction_rule(u=gamma_u_r, i=gamma_i)  # Shape: [batch_size]
        elif self.predicate == 'MF':
            # code added for testing version ( the different version)
            v = '1'
            if v == '1' and r > 0:
                score, _ = torch.min(gamma_u_r + gamma_i, 1) # min along columns
            elif v == '2' and r > 0:
                print()
            else:
                score = torch.sum(gamma_u_r * gamma_i, -1)
        else:
            raise NotImplementedError
        scores.append(score.unsqueeze(1))  # Shape: [batch_size, 1]

    # Concatenate and compute disjunction
    scores_tensor = torch.cat(scores, dim=1)  # Shape: [batch_size, n_rules]
    if self.predicate == 'AND':
        xui = self.disjunction_rule(scores_tensor)  # Shape: [batch_size]
    elif self.predicate == 'MF':
        xui = self.disjunction_rule_non_fuzzy(scores_tensor)
    else:
        raise NotImplementedError
    return xui, gu, gamma_i


Do the same but with the following function that works with disjunction and conjunction rules but in a way such that the the score is computed wrt all the user in the batch wrt ALL the items in the system. Until now it work only with positive values in (0,1 ) edit this to work as the previous two functions to work in (-1, 1)

def predict(self, start, stop, **kwargs):
    # Prediction for all items for users in the range [start, stop)
    gu = self.Gu.weight[start:stop].to(self.device)  # Shape: [num_users, n_rules * embed_k]
    gu = gu.view(gu.shape[0], self.n_rules, self.embed_k)
    # gu = torch.clamp(gu, 0.0, 1.0)

    gamma_i = self.Gi.weight.to(self.device)  # Shape: [num_items, embed_k]
    # gamma_i = torch.clamp(gamma_i, 0.0, 1.0)

    # Compute scores for each user and item
    scores = []
    for r in range(self.n_rules):
        # gu_r = gu[:, r, :]  # [num_users, embed_k]
        if self.predicate == 'AND':
            gu = torch.clamp(gu, 0.0, 1.0)
            gamma_i = torch.clamp(gamma_i, 0.0, 1.0)
            gu_r = gu[:, r, :]  # [num_users, embed_k]
            # Broadcasting over items
            expr = 1 - gu_r.unsqueeze(1) * (1 - gamma_i.unsqueeze(0)) + self.epsilon  # [num_users, num_items, embed_k]
            log_expr = torch.log(expr)
            sum_log_expr = torch.sum(log_expr, dim=2)
            score = -1.0 / (-1.0 + sum_log_expr)  # [num_users, num_items]
            scores.append(score.unsqueeze(0))  # [1, num_users, num_items]
        elif self.predicate == 'MF':
            gu_r = gu[:, r, :] # [num_users, embed_k]
            # added for testing:
            v = '1'
            if v == '1' and r > 0:
                sum_emb = gu_r.unsqueeze(1) + gamma_i
                score , _ = torch.min(sum_emb, dim=2)
            elif v == '2' and r > 0:
                print()
            else:
                score = torch.matmul(gu_r, torch.transpose(gamma_i, 0, 1))
            scores.append(score.unsqueeze(0))

    scores_tensor = torch.cat(scores, dim=0)  # [n_rules, num_users, num_items]
    if self.predicate == 'AND':
        # Combine scores using disjunction
        expr = 1 - scores_tensor + self.epsilon  # [n_rules, num_users, num_items]
        log_expr = torch.log(expr)
        sum_log_expr = torch.sum(log_expr, dim=0)  # Sum over rules
        final_scores = 1 - (-1.0 / (-1.0 + sum_log_expr))  # [num_users, num_items]
    elif self.predicate == 'MF':
        final_scores = self.disjunction_rule_non_fuzzy_matrix(scores_tensor)
    else:
        raise NotImplementedError
    return final_scores

So for example if I have 2 user in the batch and a total of 1000 items, the function will give in output a matrix of shape (n_user_in_the_batch, n_items) = (2, 1000) where each entry correspond to a score.
For example 
final_scores[0, 32] == the score for the user 0 wrt the item 32