The Self-Conscious Statisticians

Imagine a group of statisticians who are in a weight loss program. Because they’re statisticians, they want to quantify the group’s progress, but because they’re self-conscious, none want to reveal their weight publicly. This is a perfect use-case for Cicada and MPC:

[1]:
import logging

import numpy

from cicada.additive import AdditiveProtocolSuite
from cicada.communicator import SocketCommunicator
from cicada.logging import Logger

logging.basicConfig(level=logging.INFO)

def main(communicator):
    log = Logger(logging.getLogger(), communicator)
    protocol = AdditiveProtocolSuite(communicator)

    # Each player loads their weight from a file.
    weight = numpy.loadtxt(f"statistician-weight-{communicator.rank}.txt")

    # Compute the sum of the player weights.
    sum_share = protocol.share(src=0, secret=numpy.array(0), shape=())
    for rank in communicator.ranks:
        weight_share = protocol.share(src=rank, secret=weight, shape=())
        sum_share = protocol.add(sum_share, weight_share)

    # Divide by the number of players to obtain the mean weight.
    count_share = protocol.share(src=0, secret=numpy.array(communicator.world_size), shape=())
    mean_share = protocol.divide(sum_share, count_share)

    # Reveal the total and average weight to the group.
    sum = protocol.reveal(sum_share)
    mean = protocol.reveal(mean_share)

    log.info(f"Sum revealed to player {communicator.rank}: {sum}")
    log.info(f"Mean weight revealed to player {communicator.rank}: {mean}")

SocketCommunicator.run(world_size=5, fn=main);
INFO:root:Sum revealed to player 0: 904.0
INFO:root:Sum revealed to player 1: 904.0
INFO:root:Sum revealed to player 2: 904.0
INFO:root:Sum revealed to player 3: 904.0
INFO:root:Sum revealed to player 4: 904.0
INFO:root:Mean weight revealed to player 0: 180.7978515625
INFO:root:Mean weight revealed to player 1: 180.7978515625
INFO:root:Mean weight revealed to player 2: 180.7978515625
INFO:root:Mean weight revealed to player 3: 180.7978515625
INFO:root:Mean weight revealed to player 4: 180.7978515625

If we manually inspect the players’ weights, we can see that the result is correct, making allowances for the default 16-bit fixed point precision:

[2]:
weights = [numpy.loadtxt(f"statistician-weight-{rank}.txt") for rank in range(5)]

for rank, weight in enumerate(weights):
    print(f"Player {rank} weight: {weight}")

print(f"sum: {numpy.sum(weights)}")
print(f"mean: {numpy.mean(weights)}")
Player 0 weight: 130.0
Player 1 weight: 220.0
Player 2 weight: 98.0
Player 3 weight: 241.0
Player 4 weight: 215.0
sum: 904.0
mean: 180.8