Suppose we have count data \(y_1, y_2, \ldots, y_n\) that we model as independent draws from a Poisson distribution with unknown parameter \(\lambda\). The maximum likelihood estimate is \(\hat{\lambda} = \bar{y}\), obtained by minimizing the negative log-likelihood
\[ -\ell(\lambda \mid y) \;=\; -\sum_{i=1}^{n} \log p(y_i \mid \lambda). \]
In R, this is a one-liner using stats4::mle():
library(stats4)
set.seed(17822)
y <- rpois(n = 40, lambda = 10)
nLL <- function(lambda) -sum(stats::dpois(y, lambda, log = TRUE))
fit0 <- mle(nLL, start = list(lambda = 5), nobs = NROW(y))
summary(fit0)
## Maximum likelihood estimation
##
## Call:
## mle(minuslogl = nLL, start = list(lambda = 5), nobs = NROW(y))
##
## Coefficients:
## Estimate Std. Error
## lambda 9.175 0.4789311
##
## -2 log L: 199.5328
logLik(fit0)
## 'log Lik.' -99.76641 (df=1)
Now suppose the same data is distributed across three sites — say, three hospitals counting adverse events. None will share its raw counts with the others or with a central aggregator, but they are willing to jointly compute the same MLE provided no party learns anything about another party’s contribution.
To simulate this, partition y:
y1 <- y[1:20]
y2 <- y[21:27]
y3 <- y[28:40]
The negative log-likelihood factorizes additively:
\[ -\ell(\lambda \mid y) \;=\; -\ell_1(\lambda \mid y_1) - \ell_2(\lambda \mid y_2) - \ell_3(\lambda \mid y_3) \]
so each site can compute its local term in the clear and only the sum of the three local likelihoods needs to travel between parties — and the sum must not reveal the individual addends.
We use the master/worker topology that distcomp- and DataSHIELD-style federated analyses actually deploy: a star with the master at the center and one independent worker per site. There is no chain and no inter-site communication.
In words:
This is the realistic shape: each worker independently does its local computation and ships an encrypted summary; the master only sees the encrypted summaries (and, after homomorphic summation, the decrypted total). With a single-decrypter master, the master could decrypt individual \(E(\ell_i)\) in principle; the cryptographic story strengthens when paired with threshold key generation, where no single party holds the secret key. We will revisit that in the Cox threshold vignette.
The computational topology — Site (worker),
Master, the master/worker runner — is the same code as in
any other distributed-stats vignette in this package; only the master’s
backend changes. homomorpheR exports
make_ckks_master() that takes an openfhe.R
CryptoContext and KeyPair. The master decrypts
with the secret key; each site encrypts its own contribution under the
public key it is given at setup. The worker class and the
master_aggregate() runner are backend-agnostic.
The per-site negative log-likelihood is the same plain R function it would be in the cleartext case:
library(homomorpheR)
local_nll <- function(data, lambda) {
-sum(stats::dpois(data, lambda, log = TRUE))
}
We call openfhe.R functions qualified rather than
attaching openfhe.R. There is one encrypt()
and one decrypt(): they are openfhe.R’s
generics, and homomorpheR registers its methods on them and
re-exports them.
cc <- openfhe.R::fhe_context("CKKS",
multiplicative_depth = 1L,
scaling_mod_size = 50L,
batch_size = 8L)
keys <- openfhe.R::key_gen(cc)
worker1 <- make_worker("S1", data = y1, contribution_fn = local_nll)
worker2 <- make_worker("S2", data = y2, contribution_fn = local_nll)
worker3 <- make_worker("S3", data = y3, contribution_fn = local_nll)
master <- make_ckks_master("Master", crypto_context = cc, keypair = keys)
set_workers(master, list(worker1, worker2, worker3))
mle() through the encrypted channelfit1 <- mle(function(lambda) master_aggregate(master, lambda),
start = list(lambda = 5))
summary(fit1)
## Maximum likelihood estimation
##
## Call:
## mle(minuslogl = function(lambda) master_aggregate(master, lambda),
## start = list(lambda = 5))
##
## Coefficients:
## Estimate Std. Error
## lambda 9.175 0.4789311
##
## -2 log L: 199.5328
logLik(fit1)
## 'log Lik.' -99.76641 (df=1)
The CKKS-based estimate differs from the cleartext estimate
fit0 by 7.3e-12. No site ever revealed its individual
counts to any other party.
For Poisson MLE the protocol uses only additions, so the additive
Paillier scheme would also suffice. CKKS saves the integer encoding of
real numbers that Paillier needs. The same holds for stratified Cox
regression (vignette("cox")), where the sites’ partial
log-likelihoods are summed. Multiplying encrypted values is needed only
when the computation itself runs on encrypted data, as in the sigmoid of
vignette("encrypted-regression").
This is a teaching example. In production you would want a real communication transport, threshold key generation so no single party holds the full secret, and persistent serialization at site boundaries. The point of this vignette is not the deployment story but the structure of a privacy-preserving distributed computation built on homomorphic encryption.