Noise in CKKS -> FHEW scheme switching

My application of CKKS → FHEW scheme switching has a significant amount of randomness, at least with the settings I am using. For instance, a CKKS computation that decrypts to (approximately) -0.525 will mostly decrypt to -1 (technically, it will decrypt to N-1 where N is the plaintext modulus I specify for FHEW decrypt) but about 20% (!!) of the time it decrypts to 0. Is this to be expected or is it some artifact of how I am doing scheme switching?

Here is a code I was using that appears to demonstrate this:

  uint32_t multDepth    = 26;
  uint32_t scaleModSize = 50;
  SecurityLevel sl      = HEStd_128_classic;
  BINFHE_PARAMSET slBin = STD128;
  uint32_t logQ_ccLWE   = 25;
  uint32_t slots     = 16;  // sparsely-packed
  uint32_t batchSize = slots;

  CCParams<CryptoContextCKKSRNS> parameters;

  parameters.SetMultiplicativeDepth(multDepth);

  parameters.SetScalingModSize(scaleModSize);
  parameters.SetScalingTechnique(FLEXIBLEAUTOEXT);
  parameters.SetSecurityLevel(sl);
  parameters.SetBatchSize(batchSize);

  CryptoContext<DCRTPoly> cc = GenCryptoContext(parameters);

  // Enable the features that you wish to use
  cc->Enable(PKE);
  cc->Enable(KEYSWITCH);
  cc->Enable(LEVELEDSHE);
  cc->Enable(SCHEMESWITCH);

  auto keys = cc->KeyGen();

  SchSwchParams params;
  params.SetSecurityLevelCKKS(sl);
  params.SetSecurityLevelFHEW(slBin);
  params.SetCtxtModSizeFHEWLargePrec(logQ_ccLWE);
  params.SetNumSlotsCKKS(slots);
  auto privateKeyFHEW = cc->EvalCKKStoFHEWSetup(params);
  auto ccLWE          = cc->GetBinCCForSchemeSwitch();
  cc->EvalCKKStoFHEWKeyGen(keys, privateKeyFHEW);

  auto pLWE1       = 131072; 

  double scale1 = 1.0 / pLWE1;
  std::cout <<"pLWE1 is " << pLWE1 << std::endl;
  cc->EvalCKKStoFHEWPrecompute(scale1);

  std::vector<double> x1  = {0, 0.1, 0.2, 0.3, 0.4, -0.525914, 0.6, 0.7, 0.8, 0.9, 1, 2, 3, 4, 5, 6};
  uint32_t encodedLength1 = x1.size();
  Plaintext ptxt1 = cc->MakeCKKSPackedPlaintext(x1, 1, 0, nullptr);

  auto c1 = cc->Encrypt(keys.publicKey, ptxt1);
  auto cTemp = cc->EvalCKKStoFHEW(c1, encodedLength1);

  std::cout << "FHEW decryption: ";
  LWEPlaintext result;
  for (uint32_t i = 0; i < cTemp.size(); ++i) {
    ccLWE->Decrypt(privateKeyFHEW, cTemp[i], &result, pLWE1);
    std::cout << result << " ";
  }
  std::cout << "\n" << std::endl;

Output is:

 for run in {1..10}; do ./scheme-switching-simple; done
pLWE1 is 131072
FHEW decryption: 0 0 0 0 0 131071 1 1 1 1 1 2 3 4 5 6 

pLWE1 is 131072
FHEW decryption: 0 0 0 0 0 131071 1 1 1 1 1 2 3 4 5 6 

pLWE1 is 131072
FHEW decryption: 0 0 0 0 0 131071 1 1 1 1 1 2 3 4 5 6 

pLWE1 is 131072
FHEW decryption: 0 0 0 0 0 0 1 1 1 1 1 2 3 4 5 6 

pLWE1 is 131072
FHEW decryption: 0 0 0 0 0 131071 1 1 1 1 1 2 3 4 5 6 

pLWE1 is 131072
FHEW decryption: 0 0 0 0 0 131071 1 1 1 1 1 2 3 4 5 6 

pLWE1 is 131072
FHEW decryption: 0 0 0 0 0 0 1 1 1 1 1 2 3 4 5 6 

pLWE1 is 131072
FHEW decryption: 0 0 0 0 0 0 1 1 1 1 1 2 3 4 5 6 

pLWE1 is 131072
FHEW decryption: 0 0 0 0 0 131071 1 1 1 1 1 2 3 4 5 6 

pLWE1 is 131072
FHEW decryption: 0 0 0 0 1 131071 1 1 1 1 1 2 3 4 5 6 

In general, the closer to integers the values in CKKS you want to switch to FHEW, the better. During switching, the rounding is done over ciphertexts, as FHEW supports only integer messages, so it is not exact and some errors are to be expected. While in CKKS, the precision and “separation” between the message and the error is dictated by scaleModSize, in FHEW, this separation is dictated by Q/P, which is much smaller. My suggestion, if your application allows, is either to scale up (e.g., by 10) the messages before switching, or to decrease the plaintext modulus (or increase the FHEW ciphertext modulus, but that is limited by the security).

I will also look more into it to see whether there is a bug.