diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/__init__.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/__init__.py
index dabd1e8a..ce418dc1 100644
--- a/cryptographic_estimators/SDEstimator/SDAlgorithms/__init__.py
+++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/__init__.py
@@ -1,5 +1,8 @@
from .ball_collision import BallCollision
+from .bjmm_dw import BJMMdw
+from .bjmm_pdw import BJMMpdw
from .bjmm import BJMM, BJMMd2, BJMMd3
+from .bjmm_plus import BJMMplus
from .both_may import BothMay
from .dumer import Dumer
from .may_ozerov import MayOzerov, MayOzerovD2, MayOzerovD3
diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/ball_collision.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/ball_collision.py
index 62032836..f205e8db 100644
--- a/cryptographic_estimators/SDEstimator/SDAlgorithms/ball_collision.py
+++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/ball_collision.py
@@ -22,7 +22,7 @@
from ...SDEstimator.sd_helper import _gaussian_elimination_complexity, _mem_matrix, _list_merge_complexity, binom, log2, inf, min_max
from types import SimpleNamespace
from ..sd_constants import *
-#from ..SDWorkfactorModels.ball_collision import BallCollisionScipyModel
+from ..SDWorkfactorModels.ball_collision import BallCollisionScipyModel
class BallCollision(SDAlgorithm):
@@ -61,7 +61,7 @@ def __init__(self, problem: SDProblem, **kwargs):
self.set_parameter_ranges("pl", 0, min_max(10, w, s))
self.set_parameter_ranges("r", 0, n - k)
- # self.scipy_model = BallCollisionScipyModel
+ self.scipy_model = BallCollisionScipyModel
@optimal_parameter
def l(self):
diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm.py
index 32a0c1b5..05c55d04 100644
--- a/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm.py
+++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm.py
@@ -24,7 +24,7 @@
binom, log2, ceil, inf
from types import SimpleNamespace
from ..sd_constants import *
-#from ..SDWorkfactorModels.bjmm import BJMMScipyModel
+from ..SDWorkfactorModels.bjmm import BJMMScipyModel
from typing import Union
@@ -388,7 +388,7 @@ def __init__(self, problem: SDProblem, **kwargs):
super(BJMMd3, self).__init__(problem, **kwargs)
self._name = "BJMMd3"
self.initialize_parameter_ranges()
- #self.scipy_model = BJMMScipyModel
+ self.scipy_model = BJMMScipyModel
def initialize_parameter_ranges(self):
"""
diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm_dw.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm_dw.py
new file mode 100644
index 00000000..52884011
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm_dw.py
@@ -0,0 +1,306 @@
+# ****************************************************************************
+# Copyright 2023 Technology Innovation Institute
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+# ****************************************************************************
+
+
+from ...base_algorithm import optimal_parameter
+from ...SDEstimator.sd_algorithm import SDAlgorithm
+from ...SDEstimator.sd_problem import SDProblem
+from ...SDEstimator.sd_helper import _gaussian_elimination_complexity, _mem_matrix, _mitm_nn_complexity, binom, log2, \
+ ceil, inf
+from scipy.special import binom as binom_sp
+from scipy.optimize import fsolve
+from warnings import filterwarnings
+from types import SimpleNamespace
+from ..sd_constants import *
+filterwarnings("ignore", category=RuntimeWarning)
+
+
+class BJMMdw(SDAlgorithm):
+ def __init__(self, problem: SDProblem, **kwargs):
+ """
+ Construct an instance of BJMM's estimator using *d*isjoint *w*eight distributions combined with
+ MitM-nearest neighbor search. [EB22]_, [MMT11]_, [BJMM12]_.
+
+ Expected weight distribution::
+
+ +---------------------------+-------------+------------+----------+----------+----------+----------+
+ |<-+ n - k - 2 l1 - 2 l2 +->|<-+ k / 2 +->|<-+ k / 2 ->|<-+ l1 +->|<-+ l1 +->|<-+ l2 +->|<-+ l2 +->|
+ | w - 2 p - 2 w1 - 2 w2 | p | p | w1 | w1 | w2 | w2 |
+ +---------------------------+-------------+------------+----------+----------+----------+----------+
+
+
+ INPUT:
+
+ - ``problem`` -- syndrome decoding problem instance
+ - ``k`` -- dimension of the code
+ - ``w`` -- Hamming weight of error vector
+ - ``mem`` -- upper bound on the available memory (as log2), default unlimited
+ - ``hmap`` -- indicates if hashmap is being used (default: true)
+ - ``p_range`` -- interval in which the parameter p is searched (default: [0, 25], helps speeding up computation)
+ - ``memory_access`` -- specifies the memory access cost model (default: 0, choices: 0 - constant, 1 - logarithmic, 2 - square-root, 3 - cube-root or deploy custom function which takes as input the logarithm of the total memory usage)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMdw
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: BJMMdw(SDProblem(n=100,k=50,w=10))
+ BJMM estimator with disjoint weight distributions in depth 2 for syndrome decoding problem with (n,k,w) = (100,50,10) over Finite Field of size 2
+ """
+ super(BJMMdw, self).__init__(problem, **kwargs)
+ self._name = "BJMM-dw"
+ self.initialize_parameter_ranges()
+
+ def initialize_parameter_ranges(self):
+ """
+ initialize the parameter ranges for p, p1, w1, w11, w2 to start the optimisation
+ process.
+ """
+ self.set_parameter_ranges("p", 0, 25)
+ self.set_parameter_ranges("p1", 0, 20)
+ self.set_parameter_ranges("w1", 0, 10)
+ self.set_parameter_ranges("w11", 0, 10)
+ self.set_parameter_ranges("w2", 0, 5)
+
+ @optimal_parameter
+ def p(self):
+ """
+ Return the optimal parameter $p$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMdw
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMMdw(SDProblem(n=100,k=50,w=10))
+ sage: A.p()
+ 2
+ """
+ return self._get_optimal_parameter("p")
+
+ @optimal_parameter
+ def p1(self):
+ """
+ Return the optimal parameter $p1$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMdw
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMMdw(SDProblem(n=100,k=50,w=10))
+ sage: A.p1()
+ 1
+ """
+ return self._get_optimal_parameter("p1")
+
+ @optimal_parameter
+ def w1(self):
+ """
+ Return the optimal parameter $w1$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMdw
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMMdw(SDProblem(n=100,k=50,w=10))
+ sage: A.w1()
+ 0
+ """
+ return self._get_optimal_parameter("w1")
+
+ @optimal_parameter
+ def w11(self):
+ """
+ Return the optimal parameter $w11$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMdw
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMMdw(SDProblem(n=100,k=50,w=10))
+ sage: A.w11()
+ 0
+ """
+ return self._get_optimal_parameter("w11")
+
+ @optimal_parameter
+ def w2(self):
+ """
+ Return the optimal parameter $w2$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMdw
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMMdw(SDProblem(n=100,k=50,w=10))
+ sage: A.w2()
+ 0
+ """
+ return self._get_optimal_parameter("w2")
+
+ def _are_parameters_invalid(self, parameters):
+ _, k, w = self.problem.get_parameters()
+ par = SimpleNamespace(**parameters)
+
+ if par.p % 2 == 1 or par.p > w // 2 or k < par.p or \
+ par.p1 < par.p // 2 or par.p1 > w or \
+ par.w1 > w // 2 - par.p or par.w1 % 2 == 1 or \
+ par.w11 < par.w1 // 2 or par.w11 >= w or \
+ par.w2 > w // 2 - par.p - par.w1:
+ return True
+ return False
+
+ def _valid_choices(self):
+ """
+ Generator which yields on each call a new set of valid parameters based on the `_parameter_ranges` and already
+ set parameters in `_optimal_parameters`
+
+ """
+ new_ranges = self._fix_ranges_for_already_set_parameters()
+ n, k, w = self.problem.get_parameters()
+ for p in range(new_ranges["p"]["min"], min(w // 2, new_ranges["p"]["max"]) + 1, 2):
+ for p1 in range(max(new_ranges["p1"]["min"], (p + 1) // 2), new_ranges["p1"]["max"] + 1):
+ s = new_ranges["w1"]["min"]
+ for w1 in range(s - (s % 2), min(w // 2 - p, new_ranges["w1"]["max"]) + 1, 2):
+ for w11 in range(max(new_ranges["w11"]["min"], (w1 + 1) // 2), new_ranges["w11"]["max"] + 1, 2):
+ for w2 in range(new_ranges["w2"]["min"], min(w // 2 - p - w1, new_ranges["w2"]["max"]) + 1):
+ indices = {"p": p, "p1": p1, "w1": w1, "w11": w11,
+ "w2": w2, "r": self._optimal_parameters["r"]}
+ if self._are_parameters_invalid(indices):
+ continue
+ yield indices
+
+ def _choose_first_constraint_such_that_representations_cancel_out_exactly(self, parameters: dict):
+ """
+ tries to find a l1 value fulfilling the constraints
+ """
+ _, k, _ = self.problem.get_parameters()
+ par = SimpleNamespace(**parameters)
+
+ try:
+ def f(x): return 2 * log2((binom(par.p, par.p // 2) * binom(k // 2 - par.p, par.p1 - par.p // 2)) * (
+ binom_sp(x, par.w1 // 2) * binom_sp(x - par.w1, par.w11 - par.w1 // 2)) + 1) - 2 * x
+ l1_val = int(
+ fsolve(f, 2 * log2((binom(par.p, par.p // 2) * binom(k // 2 - par.p, par.p1 - par.p // 2))))[0])
+ except ValueError:
+ return -1
+
+ if f(l1_val) < 0 or f(l1_val) > 10:
+ return -1
+ return l1_val
+
+ def _choose_second_constraint_such_that_list_size_remains_constant(self, parameters: dict, list_size: float):
+ """
+ trues to find a l2 value which does not increase the list size
+ """
+ par = SimpleNamespace(**parameters)
+
+ try:
+ def f(x): return log2(list_size) + 2 * \
+ log2(binom_sp(x, par.w2) + 1) - 2 * x
+ l2_val = int(fsolve(f, 50)[0])
+ except ValueError:
+ return -1
+
+ if f(l2_val) < 0 or f(l2_val) > 10:
+ return -1
+
+ return l2_val
+
+ def _time_and_memory_complexity(self, parameters: dict, verbose_information=None):
+ """
+ Computes the expected runtime and memory consumption for a given parameter set.
+ """
+
+ n, k, w = self.problem.get_parameters()
+ par = SimpleNamespace(**parameters)
+
+ local_time, local_mem = inf, inf
+ solutions = self.problem.nsolutions
+ memory_bound = self.problem.memory_bound
+ l1_search_radius = self._adjust_radius
+ l2_search_radius = max(1, self._adjust_radius // 2)
+
+ l1_start_value = self._choose_first_constraint_such_that_representations_cancel_out_exactly(
+ parameters)
+ if l1_start_value == -1:
+ return inf, inf
+
+ for l1 in range(max(l1_start_value - l1_search_radius, par.w1, par.w11), l1_start_value + l1_search_radius):
+ if 2*l1 >= n-k or n-k-2*l1 < w:
+ continue
+
+ k1 = k // 2
+ reps = (binom(par.p, par.p // 2) * binom(k1 - par.p, par.p1 - par.p // 2)) ** 2 * (
+ binom(par.w1, par.w1 // 2) * binom(l1 - par.w1, par.w11 - par.w1 // 2)) ** 2
+ reps = max(reps, 1)
+
+ L1 = binom(k1, par.p1)
+ if self._is_early_abort_possible(log2(L1)):
+ return inf, inf
+ L12 = L1 ** 2 * binom(l1, par.w11) ** 2 // 2 ** (2 * l1)
+ L12 = max(L12, 1)
+ memory = log2((2 * L1 + L12) + _mem_matrix(n, k, par.r))
+ if memory > memory_bound:
+ continue
+
+ l2_start_value = self._choose_second_constraint_such_that_list_size_remains_constant(
+ parameters, L12)
+ if l2_start_value == -1:
+ continue
+
+ l2_max = (n - k - 2 * l1 - (w - 2 * par.p -
+ 2 * par.w1 - 2 * par.w2)) // 2
+ l2_min = par.w2
+ l2_range = [l2_start_value - l2_search_radius,
+ l2_start_value + l2_search_radius]
+ for l2 in range(max(l2_min, l2_range[0]), max(1, min(l2_max, l2_range[1]))):
+ Tp = max(
+ log2(binom(n, w)) - log2(
+ binom(n - k - 2 * l1 - 2 * l2, w - 2 * par.p - 2 * par.w1 - 2 * par.w2)) - 2 * log2(
+ binom(k1, par.p)) - 2 * log2(binom(l1, par.w1)) - 2 * log2(
+ binom(l2, par.w2)) - solutions, 0)
+ Tg = _gaussian_elimination_complexity(n, k, par.r)
+
+ T_tree = 2 * _mitm_nn_complexity(L1, 2 * l1, 2 * par.w11, self._hmap) \
+ + _mitm_nn_complexity(L12, 2 * l2, 2 * par.w2, self._hmap)
+ T_rep = int(ceil(2 ** max(2 * l1 - log2(reps), 0)))
+
+ time = Tp + log2(Tg + T_rep * T_tree)
+
+ if time < local_time:
+ local_time = time
+ local_mem = memory
+ if verbose_information is not None:
+ verbose_information[VerboseInformation.CONSTRAINTS.value] = [
+ 2 * l1, 2 * l2]
+ verbose_information[VerboseInformation.PERMUTATIONS.value] = Tp
+ verbose_information[VerboseInformation.TREE.value] = log2(
+ T_rep * T_tree)
+ verbose_information[VerboseInformation.GAUSS.value] = log2(
+ Tg)
+ verbose_information[VerboseInformation.REPRESENTATIONS.value] = reps
+ verbose_information[VerboseInformation.LISTS.value] = [log2(L1), log2(L12),
+ 2 * log2(L12) + log2(
+ binom(2 * l2, 2 * par.w2)) - 2 * l2]
+
+ return local_time, local_mem
+
+ def __repr__(self):
+ """
+ """
+ rep = "BJMM estimator with disjoint weight distributions in depth 2 for " + \
+ str(self.problem)
+ return rep
diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm_pdw.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm_pdw.py
new file mode 100644
index 00000000..488d8cb5
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm_pdw.py
@@ -0,0 +1,271 @@
+# ****************************************************************************
+# Copyright 2023 Technology Innovation Institute
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+# ****************************************************************************
+
+
+from ...base_algorithm import optimal_parameter
+from ...SDEstimator.sd_algorithm import SDAlgorithm
+from ...SDEstimator.sd_problem import SDProblem
+from ...SDEstimator.sd_helper import _gaussian_elimination_complexity, _mem_matrix, _list_merge_complexity, \
+ _mitm_nn_complexity, binom, log2, ceil, inf, min_max
+from scipy.special import binom as binom_sp
+from scipy.optimize import fsolve
+from warnings import filterwarnings
+from types import SimpleNamespace
+from ..sd_constants import *
+
+filterwarnings("ignore", category=RuntimeWarning)
+
+
+class BJMMpdw(SDAlgorithm):
+ def __init__(self, problem: SDProblem, **kwargs):
+ """
+ Construct an instance of BJMM's estimator in depth 2 using partially disjoint
+ weight, applying explicit MitM-NN search on second level [MMT11]_,
+ [BJMM12]_, [EB22]_.
+
+ Expected weight distribution::
+
+ +--------------------------+--------------------+--------------------+--------+--------+
+ | <-+ n - k - l1 - 2 l2 +->|<-+ (k + l1) / 2 +->|<-+ (k + l1) / 2 +->| l2 | l2 |
+ | w - 2 p - 2 w2 | p | p | w2 | w2 |
+ +--------------------------+--------------------+--------------------+--------+--------+
+
+
+ INPUT:
+
+ - ``problem`` -- SDProblem object including all necessary parameters
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMpdw
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: BJMMpdw(SDProblem(n=100,k=50,w=10))
+ BJMM estimator with partially disjoint weight distributions in depth 2 for syndrome decoding problem with (n,k,w) = (100,50,10) over Finite Field of size 2
+
+ """
+ super(BJMMpdw, self).__init__(problem, **kwargs)
+ self._name = "BJMM-pdw"
+ self.initialize_parameter_ranges()
+
+ def initialize_parameter_ranges(self):
+ """
+ initialize the parameter ranges for p, p1, w2 to start the optimisation
+ process.
+ """
+ _, _, w = self.problem.get_parameters()
+ s = self.full_domain
+ self.set_parameter_ranges("p", 0, min_max(30, w, s))
+ self.set_parameter_ranges("p1", 0, min_max(25, w, s))
+ self.set_parameter_ranges("w2", 0, min_max(5, w, s))
+
+ @optimal_parameter
+ def p(self):
+ """
+ Return the optimal parameter $p$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMpdw
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMMpdw(SDProblem(n=100,k=50,w=10))
+ sage: A.p()
+ 2
+ """
+ return self._get_optimal_parameter("p")
+
+ @optimal_parameter
+ def p1(self):
+ """
+ Return the optimal parameter $p1$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMpdw
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMMpdw(SDProblem(n=100,k=50,w=10))
+ sage: A.p1()
+ 1
+ """
+ return self._get_optimal_parameter("p1")
+
+ @optimal_parameter
+ def w2(self):
+ """
+ Return the optimal parameter $w2$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMdw
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMMdw(SDProblem(n=100,k=50,w=10))
+ sage: A.w2()
+ 0
+ """
+ return self._get_optimal_parameter("w2")
+
+ def _are_parameters_invalid(self, parameters: dict):
+ """
+ return if the parameter set `parameters` is invalid
+
+ """
+ _, k, w = self.problem.get_parameters()
+ par = SimpleNamespace(**parameters)
+
+ if par.p % 2 == 1 or par.p > w // 2 or k < par.p or \
+ par.p1 < par.p // 2 or par.p1 > w or \
+ par.w2 > w // 2 - par.p:
+ return True
+ return False
+
+ def _valid_choices(self):
+ """
+ Generator which yields on each call a new set of valid parameters based on the `_parameter_ranges` and already
+ set parameters in `_optimal_parameters`
+
+ """
+ new_ranges = self._fix_ranges_for_already_set_parameters()
+ _, _, w = self.problem.get_parameters()
+
+ for p in range(new_ranges["p"]["min"], min(w // 2, new_ranges["p"]["max"]) + 1, 2):
+ for p1 in range(max(new_ranges["p1"]["min"], (p + 1) // 2), min(w, new_ranges["p1"]["max"]) + 1):
+ for w2 in range(new_ranges["w2"]["min"], min(w - p1, new_ranges["w2"]["max"]) + 1):
+ indices = {"p": p, "p1": p1, "w2": w2,
+ "r": self._optimal_parameters["r"]}
+ if self._are_parameters_invalid(indices):
+ continue
+ yield indices
+
+ def _choose_first_constraint_such_that_representations_cancel_out_exactly(self, parameters: dict):
+ """
+ tries to find an optimal l1 value fulfilling its contraints
+ """
+ _, k, _ = self.problem.get_parameters()
+ par = SimpleNamespace(**parameters)
+
+ try:
+ def f(x): return log2((binom(par.p, par.p // 2) *
+ binom_sp((k + x) / 2 - par.p, par.p1 - par.p // 2))) * 2 - x
+ l1_val = int(fsolve(f, 0)[0])
+
+ if f(l1_val) < 0 or f(l1_val) > 1:
+ return -1
+ except ValueError:
+ return -1
+
+ return l1_val
+
+ def _choose_second_constraint_such_that_list_size_remains_constant(self, parameters: dict, list_size: float):
+ """
+ tries to find an optimal l2 value fulfilling its contraints
+ """
+ par = SimpleNamespace(**parameters)
+
+ try:
+ def f(x): return log2(list_size) + 2 * \
+ log2(binom_sp(x, par.w2)) - 2 * x
+ l2_val = int(fsolve(f, 0)[0])
+ if f(l2_val) < 0 or f(l2_val) > 1:
+ return -1
+ except ValueError:
+ return -1
+
+ return l2_val
+
+ def _time_and_memory_complexity(self, parameters: dict, verbose_information=None):
+ """
+ Computes the expected runtime and memory consumption for a given parameter set.
+ """
+ n, k, w = self.problem.get_parameters()
+ par = SimpleNamespace(**parameters)
+ if len(parameters.keys()) == 1:
+ return inf, inf
+
+ local_time, local_mem = inf, inf
+ solutions = self.problem.nsolutions
+ memory_bound = self.problem.memory_bound
+ l1_search_radius = max(1, self._adjust_radius // 2)
+ l2_search_radius = max(1, self._adjust_radius // 2)
+
+ l1_start_value = self._choose_first_constraint_such_that_representations_cancel_out_exactly(
+ parameters)
+ if l1_start_value == -1:
+ return inf, inf
+
+ for l1 in range(max(0, l1_start_value - l1_search_radius), l1_start_value + l1_search_radius):
+ if 2*l1 >= n-k or n-k-2*l1 < w:
+ continue
+
+ k1 = (k + l1) // 2
+
+ if k1 - par.p < 0 or k1 - par.p < par.p1 - par.p // 2:
+ continue
+ reps = (binom(par.p, par.p // 2) *
+ binom(k1 - par.p, par.p1 - par.p // 2)) ** 2
+
+ L1 = binom(k1, par.p1)
+ if self._is_early_abort_possible(log2(L1)):
+ return inf, inf
+
+ L12 = max(L1 ** 2 // 2 ** l1, 1)
+
+ memory = log2((2 * L1 + L12) + _mem_matrix(n, k, par.r))
+ if memory > memory_bound:
+ continue
+
+ l2_start_value = self._choose_second_constraint_such_that_list_size_remains_constant(
+ parameters, L12)
+ if l2_start_value == -1:
+ continue
+
+ l2_min, l2_max = par.w2, (n - k - l1 -
+ (w - 2 * par.p - 2 * par.w2)) // 2
+ l2_range = [l2_start_value - l2_search_radius,
+ l2_start_value + l2_search_radius]
+ for l2 in range(max(l2_min, l2_range[0]), min(l2_max, l2_range[1])):
+ Tp = max(
+ log2(binom(n, w)) - log2(binom(n - k - l1 - 2 * l2, w - 2 * par.p - 2 * par.w2)) - 2 * log2(
+ binom(k1, par.p)) - 2 * log2(binom(l2, par.w2)) - solutions, 0)
+ Tg = _gaussian_elimination_complexity(n, k, par.r)
+
+ T_tree = 2 * _list_merge_complexity(L1, l1, self._hmap) + _mitm_nn_complexity(L12, 2 * l2, 2 * par.w2,
+ self._hmap)
+ T_rep = int(ceil(2 ** max(l1 - log2(reps), 0)))
+
+ time = Tp + log2(Tg + T_rep * T_tree)
+ if time < local_time:
+ local_time = time
+ local_mem = memory
+ if verbose_information is not None:
+ verbose_information[VerboseInformation.CONSTRAINTS.value] = [
+ l1, 2 * l2]
+ verbose_information[VerboseInformation.PERMUTATIONS.value] = Tp
+ verbose_information[VerboseInformation.TREE.value] = log2(
+ T_rep * T_tree)
+ verbose_information[VerboseInformation.GAUSS.value] = log2(
+ Tg)
+ verbose_information[VerboseInformation.REPRESENTATIONS.value] = reps
+ verbose_information[VerboseInformation.LISTS.value] = [
+ log2(L1), log2(L12), 2 * log2(L12)]
+
+ return local_time, local_mem
+
+ def __repr__(self):
+ """
+ """
+ rep = "BJMM estimator with partially disjoint weight distributions in depth 2 for " + \
+ str(self.problem)
+ return rep
diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm_plus.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm_plus.py
new file mode 100644
index 00000000..28c27cb6
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm_plus.py
@@ -0,0 +1,248 @@
+# ****************************************************************************
+# Copyright 2023 Technology Innovation Institute
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+# ****************************************************************************
+
+
+from ...base_algorithm import optimal_parameter
+from ...SDEstimator.sd_algorithm import SDAlgorithm
+from ...SDEstimator.sd_problem import SDProblem
+from ...SDEstimator.sd_helper import _gaussian_elimination_complexity, _mem_matrix, _list_merge_complexity, min_max, \
+ binom, log2, ceil, inf, _list_merge_async_complexity
+from types import SimpleNamespace
+from ..sd_constants import *
+
+
+class BJMMplus(SDAlgorithm):
+ def __init__(self, problem: SDProblem, **kwargs):
+ """
+ Complexity estimate of BJMM+ algorithm in depth 2
+
+ This class incorporates the improvements by [EZ23]_, regarding a time-memory tradeoff which improves over the
+ BJMM algorithm in terms of memory usages.
+
+ For further reference see [MMT11]_ and [BJMM12]_.
+
+ expected weight distribution::
+
+ +--------------------------+-------------------+-------------------+
+ | <-----+ n - k - l +----->|<--+ (k + l)/2 +-->|<--+ (k + l)/2 +-->|
+ | w - 2p | p | p |
+ +--------------------------+-------------------+-------------------+
+
+ INPUT:
+
+ - ``problem`` -- SDProblem object including all necessary parameters
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMplus
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: BJMMplus(SDProblem(n=100,k=50,w=10))
+ BJMM+ estimator for syndrome decoding problem with (n,k,w) = (100,50,10) over Finite Field of size 2
+
+ """
+
+ super(BJMMplus, self).__init__(problem, **kwargs)
+ self._name = "BJMM"
+ self.initialize_parameter_ranges()
+ self.limit_depth = kwargs.get("limit_depth", False)
+ self.qc = False
+
+
+ def initialize_parameter_ranges(self):
+ """
+ initialize the parameter ranges for p, p1, l to start the optimisation
+ process.
+ """
+ n, k, w = self.problem.get_parameters()
+ s = self.full_domain
+ self.set_parameter_ranges("p", 0, min_max(35, w, s))
+ self.set_parameter_ranges("p1", 0, min_max(35, w, s))
+ self.set_parameter_ranges("l", 0, min_max(500, n - k, s))
+ self.set_parameter_ranges("l1", 0, min_max(200, n - k, s))
+
+ @optimal_parameter
+ def l(self):
+ """
+ Return the optimal parameter $l$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMplus
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMMplus(SDProblem(n=100,k=50,w=10))
+ sage: A.l()
+ 8
+
+ """
+ return self._get_optimal_parameter("l")
+
+ @optimal_parameter
+ def l1(self):
+ """
+ Return the optimal parameter $l$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMplus
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMMplus(SDProblem(n=100,k=50,w=10))
+ sage: A.l1()
+ 2
+
+ """
+ return self._get_optimal_parameter("l1")
+
+ @optimal_parameter
+ def p(self):
+ """
+ Return the optimal parameter $p$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMplus
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMMplus(SDProblem(n=100,k=50,w=10))
+ sage: A.p()
+ 2
+ """
+ return self._get_optimal_parameter("p")
+
+ @optimal_parameter
+ def p1(self):
+ """
+ Return the optimal parameter $p1$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMplus
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMMplus(SDProblem(n=100,k=50,w=10))
+ sage: A.p1()
+ 1
+ """
+ return self._get_optimal_parameter("p1")
+
+ def _are_parameters_invalid(self, parameters: dict):
+ """
+ return if the parameter set `parameters` is invalid
+
+ """
+ n, k, w = self.problem.get_parameters()
+ par = SimpleNamespace(**parameters)
+ k1 = (k + par.l) // 2
+ if par.p > w // 2 or \
+ k1 < par.p or \
+ par.l >= n - k or\
+ n - k - par.l < w - 2 * par.p or \
+ k1 - par.p < par.p1 - par.p / 2 or \
+ par.p1 < par.p / 2:
+ return True
+ return False
+
+ def _valid_choices(self):
+ """
+ Generator which yields on each call a new set of valid parameters based on the `_parameter_ranges` and already
+ set parameters in `_optimal_parameters`
+ """
+ new_ranges = self._fix_ranges_for_already_set_parameters()
+
+ n, k, w = self.problem.get_parameters()
+
+ for p in range(new_ranges["p"]["min"], min(w // 2, new_ranges["p"]["max"]) + 1, 2):
+ for l in range(new_ranges["l"]["min"], min(n - k - (w - 2 * p), new_ranges["l"]["max"]) + 1):
+ for p1 in range(max(new_ranges["p1"]["min"], (p + 1) // 2), new_ranges["p1"]["max"] + 1):
+ L1 = log2(binom((k+l)//2, p1))
+ d1 = self._adjust_radius
+ lower = new_ranges["l1"]["min"] if new_ranges["l1"]["min"] == new_ranges["l1"]["max"] else max(int(L1)-d1, 0)
+ upper = new_ranges["l1"]["max"] if new_ranges["l1"]["min"] == new_ranges["l1"]["max"] else max(int(L1)+d1, 0)
+
+ for l1 in range(lower, upper):
+ indices = {"p": p, "p1": p1, "l": l, "l1": l1,
+ "r": self._optimal_parameters["r"]}
+ if self._are_parameters_invalid(indices):
+ continue
+ yield indices
+
+ def _time_and_memory_complexity(self, parameters: dict, verbose_information=None):
+ """
+ computes the expected runtime and memory consumption for the depth 2 version
+
+ """
+ n, k, w = self.problem.get_parameters()
+ par = SimpleNamespace(**parameters)
+ k1 = (k + par.l) // 2
+
+ if self._are_parameters_invalid(parameters):
+ return inf, inf
+
+ solutions = self.problem.nsolutions
+
+ L1 = binom(k1, par.p1)
+ if self._is_early_abort_possible(log2(L1)):
+ return inf, inf
+
+ if self.qc:
+ L1b = binom(k1, par.p1 - 1) * k
+
+ if not self.qc:
+ reps = (binom(par.p, par.p // 2) * binom(k1 - par.p, par.p1 - par.p // 2)) ** 2
+ else:
+ reps = binom(par.p, par.p // 2) * binom(k1 - par.p, par.p1 - par.p // 2) * binom(k1 - par.p + 1, par.p1 - par.p // 2)
+
+ L12 = max(1, L1 ** 2 // 2 ** par.l1)
+
+ qc_advantage = 0
+ if self.qc:
+ L12b = max(1, L1 * L1b // 2 ** par.l1)
+ qc_advantage = log2(k)
+
+ memory = log2((2 * L1 + L12) + _mem_matrix(n, k, par.r)) if not self.qc else\
+ log2(L1 + L1b + min(L12, L12b) + _mem_matrix(n, k, par.r))
+ if self._is_early_abort_possible(memory):
+ return inf, inf
+
+ Tp = max(log2(binom(n, w))
+ - log2(binom(n - k - par.l, w - 2 * par.p + self.qc))
+ - log2(binom(k1, par.p))
+ - log2(binom(k1, par.p - self.qc))
+ - qc_advantage - solutions, 0)
+
+ Tg = _gaussian_elimination_complexity(n, k, par.r)
+ if not self.qc:
+ T_tree = 2 * _list_merge_complexity(L1, par.l1, self._hmap) +\
+ _list_merge_complexity(L12, par.l - par.l1, self._hmap)
+ else:
+ T_tree = _list_merge_async_complexity(L1, L1b, par.l1, self._hmap) +\
+ _list_merge_complexity(L1, par.l1, self._hmap) +\
+ _list_merge_async_complexity(L12, L12b, self._hmap)
+ T_rep = int(ceil(2 ** (par.l1 - log2(reps))))
+ time = Tp + log2(Tg + T_rep * T_tree)
+
+ if verbose_information is not None:
+ verbose_information[VerboseInformation.CONSTRAINTS.value] = [par.l1, par.l - par.l1]
+ verbose_information[VerboseInformation.PERMUTATIONS.value] = Tp
+ verbose_information[VerboseInformation.TREE.value] = log2(T_rep * T_tree)
+ verbose_information[VerboseInformation.GAUSS.value] = log2(Tg)
+ verbose_information[VerboseInformation.REPRESENTATIONS.value] = reps
+ verbose_information[VerboseInformation.LISTS.value] = [
+ log2(L1), log2(L12), 2 * log2(L12) - (par.l - par.l1)]
+
+ return time, memory
+
+ def __repr__(self):
+ rep = "BJMM+ estimator for " + str(self.problem)
+ return rep
diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/both_may.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/both_may.py
index ce425ce9..f114c689 100644
--- a/cryptographic_estimators/SDEstimator/SDAlgorithms/both_may.py
+++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/both_may.py
@@ -23,7 +23,7 @@
log2, inf, ceil
from types import SimpleNamespace
from ..sd_constants import *
-#from ..SDWorkfactorModels import BothMayScipyModel
+from ..SDWorkfactorModels import BothMayScipyModel
class BothMay(SDAlgorithm):
@@ -54,7 +54,7 @@ def __init__(self, problem: SDProblem, **kwargs):
super(BothMay, self).__init__(problem, **kwargs)
self._name = "Both-May"
self.initialize_parameter_ranges()
- #self.scipy_model = BothMayScipyModel
+ self.scipy_model = BothMayScipyModel
def initialize_parameter_ranges(self):
"""
diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/dumer.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/dumer.py
index ac8ceea1..2b000cbd 100644
--- a/cryptographic_estimators/SDEstimator/SDAlgorithms/dumer.py
+++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/dumer.py
@@ -23,7 +23,7 @@
binom, log2, inf
from types import SimpleNamespace
from ..sd_constants import *
-#from ..SDWorkfactorModels.dumer import DumerScipyModel
+from ..SDWorkfactorModels.dumer import DumerScipyModel
class Dumer(SDAlgorithm):
@@ -55,7 +55,7 @@ def __init__(self, problem: SDProblem, **kwargs):
super(Dumer, self).__init__(problem, **kwargs)
self._name = "Dumer"
self.initialize_parameter_ranges()
- #self.scipy_model = DumerScipyModel
+ self.scipy_model = DumerScipyModel
def initialize_parameter_ranges(self):
"""
diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/may_ozerov.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/may_ozerov.py
index 4c6a9694..42031a3e 100644
--- a/cryptographic_estimators/SDEstimator/SDAlgorithms/may_ozerov.py
+++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/may_ozerov.py
@@ -24,7 +24,7 @@
_indyk_motwani_complexity, min_max, binom, log2, ceil, inf
from types import SimpleNamespace
from ..sd_constants import *
-#from ..SDWorkfactorModels.may_ozerov import MayOzerovScipyModel
+from ..SDWorkfactorModels.may_ozerov import MayOzerovScipyModel
from typing import Union
@@ -365,7 +365,7 @@ def __init__(self, problem: SDProblem, **kwargs):
super(MayOzerovD3, self).__init__(problem, **kwargs)
self._name = "May-OzerovD3"
self.initialize_parameter_ranges()
- #self.scipy_model = MayOzerovScipyModel
+ self.scipy_model = MayOzerovScipyModel
def initialize_parameter_ranges(self):
"""
diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/prange.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/prange.py
index 0afa1f78..735aed7a 100644
--- a/cryptographic_estimators/SDEstimator/SDAlgorithms/prange.py
+++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/prange.py
@@ -21,7 +21,7 @@
from ...SDEstimator.sd_helper import _gaussian_elimination_complexity, _mem_matrix, binom, log2
from ...helper import ComplexityType
from ..sd_constants import *
-#from ..SDWorkfactorModels.prange import PrangeScipyModel
+from ..SDWorkfactorModels.prange import PrangeScipyModel
class Prange(SDAlgorithm):
@@ -49,7 +49,7 @@ def __init__(self, problem: SDProblem, **kwargs):
"""
self._name = "Prange"
super(Prange, self).__init__(problem, **kwargs)
- #self.scipy_model = PrangeScipyModel
+ self.scipy_model = PrangeScipyModel
def _time_and_memory_complexity(self, parameters: dict, verbose_information=None):
"""
diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/stern.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/stern.py
index 7449a52e..60fad7a0 100644
--- a/cryptographic_estimators/SDEstimator/SDAlgorithms/stern.py
+++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/stern.py
@@ -22,7 +22,7 @@
from ...SDEstimator.sd_helper import _gaussian_elimination_complexity, _mem_matrix, _list_merge_complexity, binom, log2, min_max, inf
from types import SimpleNamespace
from ..sd_constants import *
-#from ..SDWorkfactorModels.stern import SternScipyModel
+from ..SDWorkfactorModels.stern import SternScipyModel
class Stern(SDAlgorithm):
@@ -51,7 +51,7 @@ def __init__(self, problem: SDProblem, **kwargs):
self._name = "Stern"
super(Stern, self).__init__(problem, **kwargs)
self.initialize_parameter_ranges()
-# self.scipy_model = SternScipyModel
+ self.scipy_model = SternScipyModel
def initialize_parameter_ranges(self):
"""
diff --git a/cryptographic_estimators/SDEstimator/SDWorkfactorModels/__init__.py b/cryptographic_estimators/SDEstimator/SDWorkfactorModels/__init__.py
index c55c13ce..32f10ba9 100644
--- a/cryptographic_estimators/SDEstimator/SDWorkfactorModels/__init__.py
+++ b/cryptographic_estimators/SDEstimator/SDWorkfactorModels/__init__.py
@@ -1,2 +1,2 @@
- #from .scipy_model import ScipyModel
-#from .both_may import BothMayScipyModel
+from .scipy_model import ScipyModel
+from .both_may import BothMayScipyModel
\ No newline at end of file
diff --git a/cryptographic_estimators/SDEstimator/__init__.py b/cryptographic_estimators/SDEstimator/__init__.py
index a78adc69..93d30c76 100644
--- a/cryptographic_estimators/SDEstimator/__init__.py
+++ b/cryptographic_estimators/SDEstimator/__init__.py
@@ -2,4 +2,4 @@
from .sd_estimator import SDEstimator
from .sd_helper import binom, min_max, _gaussian_elimination_complexity, _optimize_m4ri, _mem_matrix, _list_merge_complexity, _indyk_motwani_complexity, _mitm_nn_complexity, _list_merge_async_complexity
from .sd_problem import SDProblem
-from .SDAlgorithms import BallCollision, BJMM, BothMay, Dumer, MayOzerov, Prange, Stern
+from .SDAlgorithms import BallCollision, BJMM, BJMMdw, BJMMpdw, BJMMplus, BothMay, Dumer, MayOzerov, Prange, Stern
diff --git a/cryptographic_estimators/__init__.py b/cryptographic_estimators/__init__.py
index 6a871fae..f72f5137 100644
--- a/cryptographic_estimators/__init__.py
+++ b/cryptographic_estimators/__init__.py
@@ -3,16 +3,16 @@
from .base_problem import BaseProblem
from .helper import ComplexityType, concat_pretty_tables, _truncate, round_or_truncate
from . import SDEstimator
-#from . import MQEstimator
-#from . import SDFqEstimator
-#from . import RegSDEstimator
-#from . import PKEstimator
-#from . import LEEstimator
-#from . import PEEstimator
-#from . import DummyEstimator
-#from . import MREstimator
-#from . import UOVEstimator
-#from . import MAYOEstimator
+from . import MQEstimator
+from . import SDFqEstimator
+from . import RegSDEstimator
+from . import PKEstimator
+from . import LEEstimator
+from . import PEEstimator
+from . import DummyEstimator
+from . import MREstimator
+from . import UOVEstimator
+from . import MAYOEstimator
# WARNING:
# This sets the MAXIMUM number of coefficients that can be calculated for any
@@ -20,6 +20,6 @@
# Do not remove the power_series import; it's needed to enforce this cap.
# The chosen limit should be sufficient for most use cases, and values
# below 4000 will raise testing errors.
-#MAX_COEFFS = 20000
-#from flint import fmpq_series as power_series, ctx
-#ctx.cap = MAX_COEFFS
+MAX_COEFFS = 20000
+from flint import fmpq_series as power_series, ctx
+ctx.cap = MAX_COEFFS
diff --git a/cryptographic_estimators/estimation_renderer.py b/cryptographic_estimators/estimation_renderer.py
index 46bbccbc..c31da9f8 100644
--- a/cryptographic_estimators/estimation_renderer.py
+++ b/cryptographic_estimators/estimation_renderer.py
@@ -20,7 +20,7 @@
from .helper import concat_all_tables, round_or_truncate
from copy import deepcopy
from prettytable import PrettyTable
-#from sage.all import RR
+from sage.all import RR
class EstimationRenderer():
@@ -128,6 +128,6 @@ def _add_rows(self, sub_table: PrettyTable, estimation: dict) -> PrettyTable:
row = [estimation[i][sub_table.title][k]
for k in sub_table.field_names]
row = [round_or_truncate(
- i, self._truncate, self._precision) for i in row]
+ i, self._truncate, self._precision) if i in RR else i for i in row]
sub_table.add_row(row)
return sub_table
diff --git a/tests.py b/tests.py
deleted file mode 100644
index 1bcc585d..00000000
--- a/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from cryptographic_estimators.SDEstimator import SDEstimator
-E = SDEstimator(n=500, k=250, w=50)
-E.table()