diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 00000000..4e246750
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,10 @@
+### Description
+- (purpose of the PR, how did you implement it)
+
+### Review process
+- (explain how to review it)
+
+### Pre-approval checklist
+- [ ] The code builds clean without any errors or warnings
+- [ ] I've added/updated tests
+- [ ] I've added/updated documentation if needed
diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
new file mode 100644
index 00000000..47f48276
--- /dev/null
+++ b/.github/workflows/build-and-test.yml
@@ -0,0 +1,43 @@
+name: build-and-test
+on:
+ pull_request:
+ branches:
+ - develop
+
+jobs:
+ build-and-run-tests:
+ runs-on: ubuntu-22.04
+ concurrency: ci-${{github.ref}}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
+
+ - name: Cache Docker layers
+ uses: actions/cache@v3
+ with:
+ path: /tmp/.buildx-cache
+ key: ${{ runner.os }}-buildx-${{ github.sha }}
+ restore-keys: |
+ ${{ runner.os }}-buildx-
+
+ - name: Build
+ uses: docker/build-push-action@v4
+ id: built-image
+ with:
+ context: .
+ push: false
+ load: true
+ tags: estimators-lib:latest
+ cache-from: type=local,src=/tmp/.buildx-cache
+ cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
+
+ - name: Move cache
+ run: |
+ rm -rf /tmp/.buildx-cache
+ mv /tmp/.buildx-cache-new /tmp/.buildx-cache
+
+ - name: Run tests
+ run: make docker-test
diff --git a/.github/workflows/build-deploy-api.yml b/.github/workflows/build-deploy-api.yml
new file mode 100644
index 00000000..da692c5e
--- /dev/null
+++ b/.github/workflows/build-deploy-api.yml
@@ -0,0 +1,69 @@
+name: build-deploy-api
+on:
+ workflow_run:
+ workflows: [build-deploy-library]
+ types: [completed]
+
+jobs:
+ api-build-deploy:
+ runs-on: ubuntu-22.04
+ concurrency: ci-${{github.ref}}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
+
+ - name: Cache Docker layers
+ uses: actions/cache@v3
+ with:
+ path: /tmp/.buildx-cache
+ key: ${{ runner.os }}-buildx-${{ github.sha }}
+ restore-keys: |
+ ${{ runner.os }}-buildx-
+
+ - name: Build library
+ uses: docker/build-push-action@v4
+ id: built-image
+ with:
+ context: .
+ push: false
+ load: true
+ tags: estimators-lib:latest
+ cache-from: type=local,src=/tmp/.buildx-cache
+ cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
+
+ - name: Move cache
+ run: |
+ rm -rf /tmp/.buildx-cache
+ mv /tmp/.buildx-cache-new /tmp/.buildx-cache
+
+ - name: Pull API from BitBucket
+ run: |
+ cd ..
+ git clone https://x-token-auth:${{ secrets.BITBUCKET_API_TOKEN }}@bitbucket.org/${{ secrets.BITBUCKET_ORG }}/${{ secrets.BITBUCKET_REPOSITORY }}.git
+ cd ${{ secrets.BITBUCKET_REPOSITORY }}
+ docker build -t estimators-api --file Dockerfile.integration --build-arg API_KEY_ARG=${{ secrets.API_KEY_ARG }} .
+
+ - name: Configure AWS Credential
+ uses: aws-actions/configure-aws-credentials@v1
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+ aws-region: me-central-1
+
+ - name: Login to Amazon ECR
+ id: login-ecr
+ uses: aws-actions/amazon-ecr-login@v1
+
+ - name: Push API to AWS
+ run: |
+ docker tag estimators-api:latest ${{ steps.login-ecr.outputs.registry }}/estimators-api:latest
+ docker push ${{ steps.login-ecr.outputs.registry }}/estimators-api:latest
+
+ - name: Reset API task
+ run: |
+ aws ecs list-tasks --region me-central-1 --cluster estimators-cluster |
+ python -c "import sys, json; print(json.load(sys.stdin)['taskArns'][0])" |
+ xargs -I{} aws ecs stop-task --task {} --region me-central-1 --cluster estimators-cluster
diff --git a/.github/workflows/build-deploy-library.yml b/.github/workflows/build-deploy-library.yml
new file mode 100644
index 00000000..784a52a7
--- /dev/null
+++ b/.github/workflows/build-deploy-library.yml
@@ -0,0 +1,67 @@
+name: build-deploy-library
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ lib-build-deploy:
+ runs-on: ubuntu-22.04
+ concurrency: ci-${{github.ref}}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
+
+ - name: Cache Docker layers
+ uses: actions/cache@v3
+ with:
+ path: /tmp/.buildx-cache
+ key: ${{ runner.os }}-buildx-${{ github.sha }}
+ restore-keys: |
+ ${{ runner.os }}-buildx-
+
+ - name: Build
+ uses: docker/build-push-action@v4
+ id: built-image
+ with:
+ context: .
+ push: false
+ load: true
+ tags: estimators-lib:latest
+ cache-from: type=local,src=/tmp/.buildx-cache
+ cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
+
+ - name: Move cache
+ run: |
+ rm -rf /tmp/.buildx-cache
+ mv /tmp/.buildx-cache-new /tmp/.buildx-cache
+
+ - name: Configure AWS Credential
+ uses: aws-actions/configure-aws-credentials@v1
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+ aws-region: us-east-1
+
+ - name: Login to Amazon ECR
+ id: login-ecr
+ uses: aws-actions/amazon-ecr-login@v1
+ with:
+ registry-type: public
+
+ - name: Tag and push to ECR
+ run: |
+ docker tag estimators-lib:latest public.ecr.aws/h0m6q0n8/estimators-lib:latest
+ docker push public.ecr.aws/h0m6q0n8/estimators-lib:latest
+
+ - name: Update documentation
+ run: sudo make docker-doc
+
+ - name: Deploy to gh-pages
+ uses: JamesIves/github-pages-deploy-action@v4
+ with:
+ folder: docs/build/html
+ branch: gh-pages
diff --git a/.github/workflows/run-pytest.yml b/.github/workflows/run-pytest.yml
new file mode 100644
index 00000000..c0975cc1
--- /dev/null
+++ b/.github/workflows/run-pytest.yml
@@ -0,0 +1,43 @@
+name: run-pytest
+on:
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ builds-and-run-pytest:
+ runs-on: ubuntu-22.04
+ concurrency: ci-${{github.ref}}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
+
+ - name: Cache Docker layers
+ uses: actions/cache@v3
+ with:
+ path: /tmp/.buildx-cache
+ key: ${{ runner.os }}-buildx-${{ github.sha }}
+ restore-keys: |
+ ${{ runner.os }}-buildx-
+
+ - name: Build
+ uses: docker/build-push-action@v4
+ id: built-image
+ with:
+ context: .
+ push: false
+ load: true
+ tags: estimators-lib:latest
+ cache-from: type=local,src=/tmp/.buildx-cache
+ cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
+
+ - name: Move cache
+ run: |
+ rm -rf /tmp/.buildx-cache
+ mv /tmp/.buildx-cache-new /tmp/.buildx-cache
+
+ - name: Run pytest
+ run: make docker-pytest
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..8eecf5bb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,61 @@
+# These are some examples of commonly ignored file patterns.
+# You should customize this list as applicable to your project.
+# Learn more about .gitignore:
+# https://www.atlassian.com/git/tutorials/saving-changes/gitignore
+
+# Node artifact files
+node_modules/
+dist/
+/build/
+_build/
+cryptographic_estimators.egg-info/
+.scannerwork
+__pycache__
+docs/build
+docs/source
+docs/make.bat
+docs/Makefile
+venv
+
+# Compiled Java class files
+*.class
+
+# Compiled Python bytecode
+*.py[cod]
+
+# Log files
+*.log
+
+# Package files
+*.jar
+
+# Maven
+target/
+dist/
+
+# JetBrains IDE
+.idea/
+
+# Unit test reports
+TEST*.xml
+
+# Generated by MacOS
+.DS_Store
+
+# Generated by Windows
+Thumbs.db
+
+# Applications
+*.app
+*.exe
+*.war
+
+# Large media files
+*.mp4
+*.tiff
+*.avi
+*.flv
+*.mov
+*.wmv
+
+.local-history/
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..1de9ab62
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,9 @@
+FROM ubuntu:22.04
+ENV DEBIAN_FRONTEND=noninteractiv
+RUN apt update && apt install -y sagemath
+RUN sage -python3 -m pip install prettytable scipy sphinx==5.3.0 furo pytest
+WORKDIR "/home/cryptographic_estimators/"
+COPY . .
+ENV SAGE_PKGS=/usr/share/sagemath/installed
+RUN sage setup.py install
+RUN sage -python3 -m pip install .
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..c4a70d5e
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,74 @@
+# Docker variables
+image_name=estimators-lib:latest
+documentation_path=$(shell pwd)
+container_name="container-for-docs"
+SAGE=sage
+
+tools:
+ @sage -python -m pip install setuptools==63.0 wheel==0.38.4 sphinx==5.3.0 furo prettytable scipy pytest
+
+lib:
+ @python3 setup.py install && sage -python -m pip install .
+
+install:
+ @make tools && make lib
+
+docker-build:
+ @docker build -t ${image_name} .
+
+docker-build-m1:
+ @docker buildx build -t ${image_name} --platform linux/x86_64 .
+
+docker-run:
+ @docker run -it --rm ${image_name}
+
+testfast:
+ @sage setup.py testfast
+
+testall: install
+ @sage setup.py testall
+
+clean-docs:
+ @rm -rf docs/build docs/source docs/make.bat docs/Makefile
+
+create-sphinx-config:
+ @sphinx-quickstart -q --sep -p TII-Estimators -a TII -l en --ext-autodoc docs
+
+create-rst-files:
+ @python3 scripts/create_documentation.py
+
+create-html-docs:
+ @sphinx-build -b html docs/source/ docs/build/html
+
+doc:
+ @make clean-docs && make create-sphinx-config && make create-rst-files && make create-html-docs
+
+add-estimator:
+ @python3 scripts/create_new_estimator.py && make add-copyright
+
+append-new-estimator:
+ @python3 scripts/append_estimator_to_input_dictionary.py
+
+stop-container-and-remove:
+ @docker stop $(container_name) && docker rm $(container_name)
+
+generate-documentation:
+ @docker exec container-for-docs make doc
+
+mount-volume-and-run:
+ @docker run --name container-for-docs --mount type=bind,source=${documentation_path}/docs,target=/home/cryptographic_estimators/docs -d -it ${image_name} sh
+
+docker-doc:
+ @make mount-volume-and-run && make generate-documentation && make stop-container-and-remove container_name="container-for-docs"
+
+docker-test:
+ @docker run --name container-for-test -d -it ${image_name} sh && docker exec container-for-test sage -t --long -T 3600 --force-lib cryptographic_estimators && docker stop container-for-test && docker rm container-for-test
+
+docker-testfast:
+ @docker run --name container-for-test -d -it ${image_name} sh && docker exec container-for-test sage -t cryptographic_estimators && make stop-container-and-remove container_name="container-for-test"
+
+add-copyright:
+ @python3 scripts/create_copyright.py
+
+docker-pytest:
+ @docker run --name pytest-estimators -d -it ${image_name} sh && docker exec pytest-estimators sh -c "sage --python3 -m pytest -vv && ${SAGE} tests/test_sdfq.sage && ${SAGE} tests/test_le_beullens.sage && ${SAGE} tests/test_le_bbps.sage && ${SAGE} tests/test_pe.sage && ${SAGE} tests/test_pk.sage" && make stop-container-and-remove container_name="pytest-estimators"
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..f5205924
--- /dev/null
+++ b/README.md
@@ -0,0 +1,61 @@
+# Cryptographic estimators
+
+## Introduction π
+
+This library provides bit security estimators and asymptotic complexity estimators for cyrptographic problems. So far it covers the binary Syndrome Decoding Problem (SDEstimator) and the Multivaritate Quadratic Problem (MQEstimator).
+
+## Getting Started π
+This project is meant to be run through a terminal or inside a docker container.
+
+---
+## Pre-requisites βοΈ
+### Local
+You would need to have Sage installed in your machine. For this follow the instructions described [here](https://www.sagemath.org/).
+### Docker
+You would need to have Docker installed in your machine. For this follow the instructions described [here](https://www.docker.com/products/docker-desktop/).
+
+---
+## Installation π
+### Local
+Once you've Sage installed you can go to this project folder and run `make install` in a terminal. This will install `cryptographic_estimators` library globally. If you encounter some permission error please try again adding `sudo` as a prefix.
+
+### Docker
+If you donβt have sage installed in your machine you can start with our dockerized app. First you will need to have running the DockerDesktop app, then open a new terminal, go to the project folder and run `make docker-build` or if you have Apple Silicon M1 Chip `make docker-build-m1`.
+
+> Note: This process may take up to 15 or 20 minutes depending on your bandwith and computer capacity.
+
+
+---
+## Running the project βοΈ
+### Local
+Open the Sage interpreter in a terminal and try importing the library as the following example.
+```python
+sage: from cryptographic_estimators.SDEstimator import SDEstimator
+sage: sd = SDEstimator(15,10,5)
+sage: sd.estimate()
+```
+### Docker
+Open a terminal and execute `make docker-run` to start the container, then you can run `sage` as if it were in local
+```python
+root@31d20617c222:/home/cryptographic_estimators# sage
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β SageMath version 9.0, Release Date: 2020-01-01 β
+β Using Python 3.8.10. Type "help()" for help. β
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+sage: from cryptographic_estimators.SDEstimator import SDEstimator
+sage:
+```
+
+---
+## Documentation π
+To generate the documentation locally you can run `make doc` and then open to `/docs/build/html/index.html` to view it. Or you can also generated the documentation through docker via running `make docker-doc`
+
+---
+## Contributing ποΈ
+The aim of this project is to be maintained by the community. We want you to help us grow this library, so please feel free to submit your pull request following the [CONTRIBUTING.md](./docs/CONTRIBUTING.md) document. Also if you need any help about how to edit the `input_dictionary.json` visit [this playground](https://github.com/Crypto-TII/cryptographic_estimators_ui).
+
+---
+
+
+
+
diff --git a/conf.py b/conf.py
new file mode 100644
index 00000000..0a6ef54e
--- /dev/null
+++ b/conf.py
@@ -0,0 +1,65 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+
+# -- Project information -----------------------------------------------------
+
+project = 'TII-Estimators'
+copyright = '2022, TII'
+author = 'TII'
+
+
+# -- General configuration ---------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx.ext.autodoc',
+]
+autoclass_content = 'both'
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = 'en'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = []
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'furo'
+html_logo = '../images/tii_logo.png'
+html_favicon = '../images/favicon.ico'
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+
+# -- Extension configuration -------------------------------------------------
diff --git a/cryptographic_estimators/DummyEstimator/DummyAlgorithms/__init__.py b/cryptographic_estimators/DummyEstimator/DummyAlgorithms/__init__.py
new file mode 100644
index 00000000..41266369
--- /dev/null
+++ b/cryptographic_estimators/DummyEstimator/DummyAlgorithms/__init__.py
@@ -0,0 +1 @@
+from .dummy_algorithm1 import DummyAlgorithm1
\ No newline at end of file
diff --git a/cryptographic_estimators/DummyEstimator/DummyAlgorithms/dummy_algorithm1.py b/cryptographic_estimators/DummyEstimator/DummyAlgorithms/dummy_algorithm1.py
new file mode 100644
index 00000000..c8fa9675
--- /dev/null
+++ b/cryptographic_estimators/DummyEstimator/DummyAlgorithms/dummy_algorithm1.py
@@ -0,0 +1,145 @@
+# ****************************************************************************
+# 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 ..dummy_algorithm import DummyAlgorithm
+from ..dummy_problem import DummyProblem
+from ...base_algorithm import optimal_parameter
+from math import log2
+
+
+class DummyAlgorithm1(DummyAlgorithm):
+ """
+ Construct an instance of DummyAlgorithm1 estimator
+
+ Add reference to correponding paper here.
+
+ INPUT:
+
+ - ``problem`` -- DummyProblem object including all necessary parameters
+ - ``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)
+ - ``complexity_type`` -- complexity type to consider (0: estimate, 1: tilde O complexity, default: 0)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.DummyEstimator.DummyAlgorithms.dummy_algorithm1 import DummyAlgorithm1
+ sage: from cryptographic_estimators.DummyEstimator.dummy_problem import DummyProblem
+ sage: E = DummyAlgorithm1(DummyProblem(100, 50))
+ sage: E
+ dummy_algorithm1 estimator for the dummy problem with parameters 100 and 50
+
+ """
+
+ def __init__(self, problem: DummyProblem, **kwargs):
+ super().__init__(problem, **kwargs)
+
+ self._name = "dummy_algorithm1"
+ problem_par1, problem_par2 = self.problem.get_parameters()
+ self.set_parameter_ranges('optimization_parameter_1', 1, problem_par1)
+ self.set_parameter_ranges(
+ 'optimization_parameter_2', 1, max(1, problem_par1 - problem_par2))
+ self.set_parameter_ranges('optimization_parameter_3', 10, 20)
+
+ @optimal_parameter
+ def optimization_parameter_3(self):
+ """
+ Return the optimal parameter $optimization_parameter_3$
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.DummyEstimator.DummyAlgorithms.dummy_algorithm1 import DummyAlgorithm1
+ sage: from cryptographic_estimators.DummyEstimator.dummy_problem import DummyProblem
+ sage: E = DummyAlgorithm1(DummyProblem(100, 50))
+ sage: E.optimization_parameter_3()
+ 10
+ """
+
+ # first define parameters that can be optimized independently from each other
+ problem_par1, problem_par2 = self.problem.get_parameters()
+ if problem_par1 - problem_par2 > 20:
+ return 10
+ else:
+ return 20
+
+ @optimal_parameter
+ def optimization_parameter_1(self):
+ """
+ Return the optimal parameter $optimization_parameter_1$
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.DummyEstimator.DummyAlgorithms.dummy_algorithm1 import DummyAlgorithm1
+ sage: from cryptographic_estimators.DummyEstimator.dummy_problem import DummyProblem
+ sage: E = DummyAlgorithm1(DummyProblem(100, 50))
+ sage: E.optimization_parameter_1()
+ 25
+ """
+
+ # then define all dependent parameters which need to be optimized together using the _get_optimal_parameter
+ # method
+ return self._get_optimal_parameter('optimization_parameter_1')
+
+ @optimal_parameter
+ def optimization_parameter_2(self):
+ """
+ Return the optimal parameter $optimization_parameter_2$
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.DummyEstimator.DummyAlgorithms.dummy_algorithm1 import DummyAlgorithm1
+ sage: from cryptographic_estimators.DummyEstimator.dummy_problem import DummyProblem
+ sage: E = DummyAlgorithm1(DummyProblem(100, 50))
+ sage: E.optimization_parameter_2()
+ 50
+ """
+
+ return self._get_optimal_parameter('optimization_parameter_2')
+
+ def _compute_time_complexity(self, parameters: dict):
+ """
+ Return the time complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ """
+ p1 = parameters["optimization_parameter_1"]
+ p2 = parameters["optimization_parameter_2"]
+ p3 = parameters["optimization_parameter_3"]
+ prob_par1, prob_par2 = self.problem.get_parameters()
+
+ time = max((16 * (prob_par1 - p1 - p2) ** 2 + (prob_par2 + 2 * p3) ** 3) * 2 ** max(0, prob_par1 - p1 - p2 - p3),
+ 2 ** p1 * p2 * p3)
+
+ return log2(time)
+
+ def _compute_memory_complexity(self, parameters: dict):
+ """
+ Return the memory complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ """
+
+ p1 = parameters["optimization_parameter_1"]
+ p2 = parameters["optimization_parameter_2"]
+ p3 = parameters["optimization_parameter_3"]
+ memory = 2 ** p1 * p2 * p3
+ return log2(memory)
diff --git a/cryptographic_estimators/DummyEstimator/__init__.py b/cryptographic_estimators/DummyEstimator/__init__.py
new file mode 100644
index 00000000..345bbe6f
--- /dev/null
+++ b/cryptographic_estimators/DummyEstimator/__init__.py
@@ -0,0 +1,4 @@
+from .dummy_algorithm import DummyAlgorithm
+from .dummy_estimator import DummyEstimator
+from .dummy_problem import DummyProblem
+from .DummyAlgorithms import *
\ No newline at end of file
diff --git a/cryptographic_estimators/DummyEstimator/dummy_algorithm.py b/cryptographic_estimators/DummyEstimator/dummy_algorithm.py
new file mode 100644
index 00000000..8f8341bf
--- /dev/null
+++ b/cryptographic_estimators/DummyEstimator/dummy_algorithm.py
@@ -0,0 +1,42 @@
+# ****************************************************************************
+# 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 BaseAlgorithm
+from .dummy_problem import DummyProblem
+
+
+class DummyAlgorithm(BaseAlgorithm):
+ def __init__(self, problem: DummyProblem, **kwargs):
+ """
+ Base class for Dummy algorithms complexity estimator
+
+ INPUT:
+
+ - ``problem`` -- DummyProblem object including all necessary parameters
+ - ``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)
+ - ``complexity_type`` -- complexity type to consider (0: estimate, 1: tilde O complexity, default: 0)
+
+ """
+ super(DummyAlgorithm, self).__init__(problem, **kwargs)
+
+ def __repr__(self):
+ """
+ NOTE: self._name must be instanciated via the child class
+ """
+ par1, par2 = self.problem.get_parameters()
+ return f"{self._name} estimator for the dummy problem with parameters {par1} and {par2} "
diff --git a/cryptographic_estimators/DummyEstimator/dummy_estimator.py b/cryptographic_estimators/DummyEstimator/dummy_estimator.py
new file mode 100644
index 00000000..7aa2bc3e
--- /dev/null
+++ b/cryptographic_estimators/DummyEstimator/dummy_estimator.py
@@ -0,0 +1,59 @@
+# ****************************************************************************
+# 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 .dummy_algorithm import DummyAlgorithm
+from .dummy_problem import DummyProblem
+from ..base_estimator import BaseEstimator
+from math import inf
+
+
+class DummyEstimator(BaseEstimator):
+ """
+ Construct an instance of DummyEstimator
+
+ INPUT:
+
+ - ``problem_parameter1`` -- First parameter of the problem
+ - ``problem_parameter2`` -- Second parameter of the problem
+ - ``memory_bound`` -- 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)
+ - ``nsolutions`` -- number of solutions of the problem in logarithmic scale
+
+ """
+
+ def __init__(self, problem_parameter1: float, problem_parameter2, memory_bound=inf, **kwargs):
+ super(DummyEstimator, self).__init__(DummyAlgorithm, DummyProblem(problem_parameter1=problem_parameter1,
+ problem_parameter2=problem_parameter2,
+ memory_bound=memory_bound, **kwargs), **kwargs)
+
+ def table(self, show_quantum_complexity=0, show_tilde_o_time=0,
+ show_all_parameters=0, precision=1, truncate=0):
+ """
+ Print table describing the complexity of each algorithm and its optimal parameters
+
+ INPUT:
+
+ - ``show_quantum_complexity`` -- show quantum time complexity (default: false)
+ - ``show_tilde_o_time`` -- show Ε time complexity (default: false)
+ - ``show_all_parameters`` -- show all optimization parameters (default: false)
+ - ``precision`` -- number of decimal digits output (default: 1)
+ - ``truncate`` -- truncate rather than round the output (default: false)
+ """
+ super(DummyEstimator, self).table(show_quantum_complexity=show_quantum_complexity,
+ show_tilde_o_time=show_tilde_o_time,
+ show_all_parameters=show_all_parameters,
+ precision=precision, truncate=truncate)
diff --git a/cryptographic_estimators/DummyEstimator/dummy_problem.py b/cryptographic_estimators/DummyEstimator/dummy_problem.py
new file mode 100644
index 00000000..d00235ab
--- /dev/null
+++ b/cryptographic_estimators/DummyEstimator/dummy_problem.py
@@ -0,0 +1,96 @@
+# ****************************************************************************
+# 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_problem import BaseProblem
+from math import log2
+
+
+class DummyProblem(BaseProblem):
+ """
+ Construct an instance of DummyProblem. Contains the parameters to optimize
+ over.
+
+ INPUT:
+
+ - ``problem_parameter1`` -- First parameter of the problem
+ - ``problem_parameter2`` -- Second parameter of the problem
+ - ``nsolutions`` -- number of solutions of the problem in logarithmic scale
+ """
+
+ def __init__(self, problem_parameter1: float, problem_parameter2: float, **kwargs):
+ super().__init__(**kwargs)
+
+ # implement restrictions if apply e.g.
+ if problem_parameter1 < problem_parameter2:
+ raise ValueError(
+ "Parameter1 needs to be larger or equal than Parameter2")
+
+ self.parameters["Parameter1"] = problem_parameter1
+ self.parameters["Parameter2"] = problem_parameter2
+
+ self.nsolutions = kwargs.get("nsolutions", max(
+ self.expected_number_solutions(), 0))
+
+ def to_bitcomplexity_time(self, basic_operations: float):
+ """
+ Returns the bit-complexity corresponding to a certain amount of basic_operations
+
+ INPUT:
+
+ - ``basic_operations`` -- Number of basic operations (logarithmic)
+
+ """
+ p1 = self.parameters["Parameter1"]
+ bit_complexity_of_one_basic_operation = log2(p1) + 4
+ return basic_operations + bit_complexity_of_one_basic_operation
+
+ def to_bitcomplexity_memory(self, elements_to_store: float):
+ """
+ Returns the memory bit-complexity associated to a given number of elements to store
+
+ INPUT:
+
+ - ``elements_to_store`` -- number of memory operations (logarithmic)
+
+ """
+ logarithm_of_bits_required_to_store_one_basic_element = 4
+ return elements_to_store + logarithm_of_bits_required_to_store_one_basic_element
+
+ def expected_number_solutions(self):
+ """
+ Returns the logarithm of the expected number of existing solutions to the problem
+
+ """
+ return self.parameters["Parameter1"] - self.parameters["Parameter2"]
+
+ def get_parameters(self):
+ """
+ Returns the optimizations parameters
+ """
+ par1 = self.parameters["Parameter1"]
+ par2 = self.parameters["Parameter2"]
+ return par1, par2
+
+ def __repr__(self):
+ """
+ """
+ par1, par2 = self.get_parameters()
+ rep = "dummy problem with (problem_parameter1, problem_parameter2) = " \
+ + "(" + str(par1) + "," + str(par2) + ")"
+
+ return rep
diff --git a/cryptographic_estimators/LEEstimator/LEAlgorithms/__init__.py b/cryptographic_estimators/LEEstimator/LEAlgorithms/__init__.py
new file mode 100644
index 00000000..5a79f2b7
--- /dev/null
+++ b/cryptographic_estimators/LEEstimator/LEAlgorithms/__init__.py
@@ -0,0 +1,3 @@
+from .leon import Leon
+from .beullens import Beullens
+from .bbps import BBPS
\ No newline at end of file
diff --git a/cryptographic_estimators/LEEstimator/LEAlgorithms/bbps.py b/cryptographic_estimators/LEEstimator/LEAlgorithms/bbps.py
new file mode 100644
index 00000000..fc6fe25a
--- /dev/null
+++ b/cryptographic_estimators/LEEstimator/LEAlgorithms/bbps.py
@@ -0,0 +1,152 @@
+from ..le_algorithm import LEAlgorithm
+from ..le_problem import LEProblem
+from ..le_constants import *
+from ...base_algorithm import optimal_parameter
+from ...PEEstimator.pe_helper import gv_distance
+from math import log2, inf, log, comb as binom, factorial
+from ...SDFqEstimator.sdfq_estimator import SDFqEstimator, SDFqProblem
+from ...base_constants import BASE_BIT_COMPLEXITIES, BASE_MEMORY_BOUND, BASE_NSOLUTIONS
+
+
+class BBPS(LEAlgorithm):
+
+ def __init__(self, problem: LEProblem, **kwargs):
+ """
+ Complexity estimate of [BBPS20]_ algorithm.
+
+ Estimates are adapted versions of the scripts derived in [BBPS20]_ with the code accessible at
+ https://github.com/paolo-santini/LESS_project
+
+ INPUT:
+
+ - ``problem`` -- PEProblem object including all necessary parameters
+ - ``sd_parameters`` -- dictionary of parameters for SDFqEstimator used as a subroutine (default: {})
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.LEEstimator.LEAlgorithms import BBPS
+ sage: from cryptographic_estimators.LEEstimator import LEProblem
+ sage: BBPS(LEProblem(30,20,251))
+ BBPS estimator for permutation equivalence problem with (n,k,q) = (30,20,251)
+
+ """
+ super().__init__(problem, **kwargs)
+ self._name = "BBPS"
+ n, k, q = self.problem.get_parameters()
+
+ self.set_parameter_ranges('w_prime', gv_distance(n, k, q), n - k + 2)
+ self.set_parameter_ranges('w', gv_distance(n, k, q), n)
+
+ self._SDFqEstimator_parameters = kwargs.get(LE_SD_PARAMETERS, {})
+ self._SDFqEstimator_parameters.pop(BASE_BIT_COMPLEXITIES, None)
+ self._SDFqEstimator_parameters.pop(BASE_NSOLUTIONS, None)
+ self._SDFqEstimator_parameters.pop(BASE_MEMORY_BOUND, None)
+
+ @optimal_parameter
+ def w(self):
+ """
+ Return the optimal parameter $w$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.LEEstimator.LEAlgorithms import BBPS
+ sage: from cryptographic_estimators.LEEstimator import LEProblem
+ sage: A = BBPS(LEProblem(30,20,251))
+ sage: A.w()
+ 14
+
+ """
+ return self._get_optimal_parameter("w")
+
+ @optimal_parameter
+ def w_prime(self):
+ """
+ Return the optimal parameter $w_prime$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.LEEstimator.LEAlgorithms import BBPS
+ sage: from cryptographic_estimators.LEEstimator import LEProblem
+ sage: A = BBPS(LEProblem(30,20,251))
+ sage: A.w_prime()
+ 10
+
+ """
+ return self._get_optimal_parameter("w_prime")
+
+ def _are_parameters_invalid(self, parameters: dict):
+ w = parameters["w"]
+ w_prime = parameters["w_prime"]
+ n, k, q = self.problem.get_parameters()
+
+ if w < w_prime + 1 or w > 2 * w_prime - 1 or w_prime > n - k:
+ return True
+ return False
+
+ def _time_and_memory_complexity(self, parameters, verbose_information=None):
+ """
+ Return time complexity of BBPS algorithm
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including parameters
+ - ``verbose_information`` -- if set to a dictionary within `Nw_prime`, c_isd` and `lists` will be returned.
+
+ """
+ w = parameters["w"]
+ w_prime = parameters["w_prime"]
+
+ n, k, q = self.problem.get_parameters()
+ Nw_prime = (log2(binom(n, w_prime)) + log2(q - 1) * (w_prime - 1) + log2(q) * (k - n))
+ if Nw_prime < 0:
+ return inf, inf
+
+ pr_w_w_prime = log2(binom(w_prime, 2 * w_prime - w)) + log2(binom(n - w_prime, w - w_prime)) - log2(
+ binom(n, w_prime)) # zeta probability in the paper
+
+ L_prime = (1 + Nw_prime * 2 - pr_w_w_prime + log2((2 * log(n)))) / 4
+ if L_prime > Nw_prime:
+ return inf, inf
+
+ pw = -1 + log2(binom(n, w - w_prime)) + log2(binom(n - (w - w_prime), w - w_prime)) \
+ + log2(binom(n - 2 * (w - w_prime), 2 * w_prime - w)) + log2(factorial(2 * w_prime - w)) \
+ + log2((q - 1)) * (w - 2 * w_prime + 1) - (log2(binom(n, w_prime)) + log2(binom(n - w_prime, w - w_prime))
+ + log2(binom(w_prime, 2 * w_prime - w)))
+
+ M_second = pr_w_w_prime + L_prime * 4 - 2 + pw + log2(2 ** pr_w_w_prime - 2 / ((2 ** Nw_prime) ** 2))
+ if M_second > 0:
+ return inf, inf
+
+ self.SDFqEstimator = SDFqEstimator(n=n, k=k, w=w_prime, q=q, bit_complexities=0, nsolutions=0,
+ memory_bound=self.problem.memory_bound, **self._SDFqEstimator_parameters)
+ c_isd = self.SDFqEstimator.fastest_algorithm().time_complexity()
+
+ time = c_isd + L_prime - Nw_prime
+ # accounting for sampling L_prime different elements from set of Nw_prime elements
+ if L_prime > Nw_prime - 1:
+ time += log2(L_prime)
+
+ if verbose_information is not None:
+ verbose_information[VerboseInformation.NW.value] = Nw_prime
+ verbose_information[VerboseInformation.LISTS.value] = L_prime
+ verbose_information[VerboseInformation.ISD.value] = c_isd
+
+ return time, self.SDFqEstimator.fastest_algorithm().memory_complexity()
+
+ def _compute_time_complexity(self, parameters):
+ return self._time_and_memory_complexity(parameters)[0]
+
+ def _compute_memory_complexity(self, parameters):
+ return self._time_and_memory_complexity(parameters)[1]
+
+ def _get_verbose_information(self):
+ """
+ returns a dictionary containing additional algorithm information
+ """
+ verb = dict()
+ _ = self._time_and_memory_complexity(self.optimal_parameters(), verbose_information=verb)
+ return verb
+
+ def __repr__(self):
+ rep = "BBPS estimator for " + str(self.problem)
+ return rep
diff --git a/cryptographic_estimators/LEEstimator/LEAlgorithms/beullens.py b/cryptographic_estimators/LEEstimator/LEAlgorithms/beullens.py
new file mode 100644
index 00000000..59aa6bd1
--- /dev/null
+++ b/cryptographic_estimators/LEEstimator/LEAlgorithms/beullens.py
@@ -0,0 +1,103 @@
+from ..le_algorithm import LEAlgorithm
+from ..le_problem import LEProblem
+from ..le_constants import *
+from ...base_algorithm import optimal_parameter
+from ...PEEstimator.pe_helper import median_size_of_random_orbit
+from ..le_helper import cost_to_find_random_2dim_subcodes_with_support_w
+from math import log2, inf, ceil, log, comb as binom
+
+
+class Beullens(LEAlgorithm):
+
+ def __init__(self, problem: LEProblem, **kwargs):
+ """
+ Complexity estimate of Beullens algorithm
+
+ Estimates are adapted versions of the scripts derived in [Beu20]_ with the code accessible at
+ https://github.com/WardBeullens/LESS_Attack
+
+ INPUT:
+
+ - ``problem`` -- LEProblem object including all necessary parameters
+ - ``sd_parameters`` -- dictionary of parameters for SDFqEstimator used as a subroutine (default: {})
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.LEEstimator.LEAlgorithms import Beullens
+ sage: from cryptographic_estimators.LEEstimator import LEProblem
+ sage: Beullens(LEProblem(n=100,k=50,q=3))
+ Beullens estimator for permutation equivalence problem with (n,k,q) = (100,50,3)
+
+ """
+ super().__init__(problem, **kwargs)
+ self._name = "Beullens"
+ n, k, _ = self.problem.get_parameters()
+ self.set_parameter_ranges('w', 0, n-k+1)
+
+ @optimal_parameter
+ def w(self):
+ """
+ Return the optimal parameter $w$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.LEEstimator.LEAlgorithms import Beullens
+ sage: from cryptographic_estimators.LEEstimator import LEProblem
+ sage: A = Beullens(LEProblem(n=100,k=50,q=3))
+ sage: A.w()
+ 34
+
+ """
+ return self._get_optimal_parameter("w")
+
+ def _time_and_memory_complexity(self, parameters, verbose_information=None):
+ """
+ Return time and memory complexity of Beulens algorithm for given parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including parameters
+ - ``verbose_information`` -- if set to a dictionary with additional information will be returned.
+
+ """
+ n, k, q = self.problem.get_parameters()
+ w = parameters["w"]
+
+ search_space_size = log2(binom(n, w)) + log2(q) * (2 * (w - 2) - 2 * (n - k))
+ if search_space_size < 0:
+ return inf, inf
+
+ size_of_orbit = median_size_of_random_orbit(n, w, q) + log2(q - 1) * (w - 1)
+ if size_of_orbit > log2(q) * (2 * (n - k)) - log2(ceil(4 * log2(n))):
+ return inf, inf
+
+ list_size = (search_space_size + log2(2 * log2(n))) / 2
+ list_computation = cost_to_find_random_2dim_subcodes_with_support_w(n, k, w) \
+ - search_space_size + list_size + 1
+
+ normal_form_cost = 1 + log2(q) + list_size
+
+ if verbose_information is not None:
+ verbose_information[VerboseInformation.LISTS_SIZE.value] = list_size
+ verbose_information[VerboseInformation.LISTS.value] = list_computation
+ verbose_information[VerboseInformation.NORMAL_FORM.value] = normal_form_cost
+
+ return max(list_computation, normal_form_cost) + log2(n), list_size + log2(n)
+
+ def _compute_time_complexity(self, parameters: dict):
+ return self._time_and_memory_complexity(parameters)[0]
+
+ def _compute_memory_complexity(self, parameters: dict):
+ return self._time_and_memory_complexity(parameters)[1]
+
+ def _get_verbose_information(self):
+ """
+ returns a dictionary containing additional algorithm information
+ """
+ verb = dict()
+ _ = self._time_and_memory_complexity(self.optimal_parameters(), verbose_information=verb)
+ return verb
+
+ def __repr__(self):
+ rep = "Beullens estimator for " + str(self.problem)
+ return rep
diff --git a/cryptographic_estimators/LEEstimator/LEAlgorithms/leon.py b/cryptographic_estimators/LEEstimator/LEAlgorithms/leon.py
new file mode 100644
index 00000000..94517d92
--- /dev/null
+++ b/cryptographic_estimators/LEEstimator/LEAlgorithms/leon.py
@@ -0,0 +1,36 @@
+from ..le_algorithm import LEAlgorithm
+from ..le_problem import LEProblem
+from ...PEEstimator import Leon as PELeon
+from ...PEEstimator.pe_problem import PEProblem
+
+
+class Leon(PELeon, LEAlgorithm):
+
+ def __init__(self, problem: LEProblem, **kwargs):
+ """
+ Complexity estimate of Leons algorithm [Leo82]_
+ Estimates are adapted versions of the scripts derived in [Beu20]_ with the code accessible at
+ https://github.com/WardBeullens/LESS_Attack
+
+ INPUT:
+
+ - ``problem`` -- PEProblem object including all necessary parameters
+ - ``codewords_needed_for_success`` -- Number of low word codewords needed for success (default = 100)
+ - ``sd_parameters`` -- dictionary of parameters for SDFqEstimator used as a subroutine (default: {})
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.LEEstimator.LEAlgorithms import Leon
+ sage: from cryptographic_estimators.LEEstimator import LEProblem
+ sage: Leon(LEProblem(n=100,k=50,q=3))
+ Leon estimator for permutation equivalence problem with (n,k,q) = (100,50,3)
+
+ """
+ LEAlgorithm.__init__(self, problem, **kwargs)
+ self._name = "Leon"
+ n, k, q = self.problem.get_parameters()
+ PELeon.__init__(self, PEProblem(n=n, k=k, q=q), **kwargs)
+
+ def __repr__(self):
+ rep = "Leon estimator for " + str(self.problem)
+ return rep
diff --git a/cryptographic_estimators/LEEstimator/__init__.py b/cryptographic_estimators/LEEstimator/__init__.py
new file mode 100644
index 00000000..4be6c622
--- /dev/null
+++ b/cryptographic_estimators/LEEstimator/__init__.py
@@ -0,0 +1,6 @@
+from .le_algorithm import LEAlgorithm
+from .le_estimator import LEEstimator
+from .le_problem import LEProblem
+from .LEAlgorithms import Leon
+from .LEAlgorithms import Beullens
+from .LEAlgorithms import BBPS
\ No newline at end of file
diff --git a/cryptographic_estimators/LEEstimator/le_algorithm.py b/cryptographic_estimators/LEEstimator/le_algorithm.py
new file mode 100644
index 00000000..53ed0c7e
--- /dev/null
+++ b/cryptographic_estimators/LEEstimator/le_algorithm.py
@@ -0,0 +1,38 @@
+# ****************************************************************************
+# 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 BaseAlgorithm
+from .le_problem import LEProblem
+
+
+class LEAlgorithm(BaseAlgorithm):
+ def __init__(self, problem: LEProblem, **kwargs):
+ """
+ Base class for SD algorithms complexity estimator
+
+ INPUT:
+
+ - ``problem`` -- LEProblem object including all necessary parameters
+
+ """
+ super(LEAlgorithm, self).__init__(problem, **kwargs)
+
+ def __repr__(self):
+ """
+ """
+ pass
diff --git a/cryptographic_estimators/LEEstimator/le_constants.py b/cryptographic_estimators/LEEstimator/le_constants.py
new file mode 100644
index 00000000..33bb9965
--- /dev/null
+++ b/cryptographic_estimators/LEEstimator/le_constants.py
@@ -0,0 +1,16 @@
+from enum import Enum
+
+
+LE_CODE_LENGTH = "code length"
+LE_CODE_DIMENSION = "code dimension"
+LE_FIELD_SIZE = "field size"
+LE_SD_PARAMETERS="sd_parameters"
+
+class VerboseInformation(Enum):
+ """
+ """
+ NW = "Nw_prime"
+ LISTS = "L_prime"
+ LISTS_SIZE = "list_size"
+ NORMAL_FORM = "normal_form"
+ ISD = "C_ISD"
diff --git a/cryptographic_estimators/LEEstimator/le_estimator.py b/cryptographic_estimators/LEEstimator/le_estimator.py
new file mode 100644
index 00000000..797e29ca
--- /dev/null
+++ b/cryptographic_estimators/LEEstimator/le_estimator.py
@@ -0,0 +1,94 @@
+# ****************************************************************************
+# 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 ..LEEstimator.le_algorithm import LEAlgorithm
+from ..LEEstimator.le_problem import LEProblem
+from ..base_estimator import BaseEstimator
+from math import inf
+
+class LEEstimator(BaseEstimator):
+ """
+ Construct an instance of the Linear Code Equivalence Estimator
+
+ INPUT:
+
+ - ``n`` -- code length
+ - ``k`` -- code dimension
+ - ``q`` -- field size
+ - ``excluded_algorithms`` -- a list/tuple of excluded algorithms (default: None)
+ - ``nsolutions`` -- no. of solutions
+
+ """
+ excluded_algorithms_by_default = []
+
+ def __init__(self, n: int, k: int, q: int, memory_bound=inf, **kwargs): # Add estimator parameters
+ if not kwargs.get("excluded_algorithms"):
+ kwargs["excluded_algorithms"] = []
+
+ kwargs["excluded_algorithms"] += self.excluded_algorithms_by_default
+ super(LEEstimator, self).__init__(
+ LEAlgorithm, LEProblem(n, k, q, memory_bound=memory_bound, **kwargs), **kwargs)
+
+ def table(self, show_quantum_complexity=0, show_tilde_o_time=0,
+ show_all_parameters=0, precision=1, truncate=0):
+ """
+ Print table describing the complexity of each algorithm and its optimal parameters
+
+ INPUT:
+
+ - ``show_quantum_complexity`` -- show quantum time complexity (default: true)
+ - ``show_tilde_o_time`` -- show Ε time complexity (default: true)
+ - ``show_all_parameters`` -- show all optimization parameters (default: true)
+ - ``precision`` -- number of decimal digits output (default: 1)
+ - ``truncate`` -- truncate rather than round the output (default: false)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.LEEstimator import LEEstimator
+ sage: A = LEEstimator(n=30, k=20, q=251)
+ sage: A.table(show_all_parameters=1)
+ +-----------+------------------------------------------+
+ | | estimate |
+ +-----------+------+--------+--------------------------+
+ | algorithm | time | memory | parameters |
+ +-----------+------+--------+--------------------------+
+ | Leon | 34.2 | 12.2 | {'w': 9} |
+ | Beullens | 29.7 | 14.4 | {'w': 11} |
+ | BBPS | 27.6 | 12.2 | {'w': 14, 'w_prime': 10} |
+ +-----------+------+--------+--------------------------+
+
+ TESTS::
+
+ sage: from cryptographic_estimators.LEEstimator import LEEstimator
+ sage: A = LEEstimator(n=200, k=110, q=31)
+ sage: A.table(precision=3, show_all_parameters=1) # long time
+ +-----------+----------------------------------------------+
+ | | estimate |
+ +-----------+---------+--------+---------------------------+
+ | algorithm | time | memory | parameters |
+ +-----------+---------+--------+---------------------------+
+ | Leon | 105.356 | 33.624 | {'w': 59} |
+ | Beullens | 123.109 | 42.252 | {'w': 79} |
+ | BBPS | 97.495 | 33.624 | {'w': 102, 'w_prime': 60} |
+ +-----------+---------+--------+---------------------------+
+
+ """
+ super(LEEstimator, self).table(show_quantum_complexity=show_quantum_complexity,
+ show_tilde_o_time=show_tilde_o_time,
+ show_all_parameters=show_all_parameters,
+ precision=precision, truncate=truncate)
diff --git a/cryptographic_estimators/LEEstimator/le_helper.py b/cryptographic_estimators/LEEstimator/le_helper.py
new file mode 100644
index 00000000..16ae7051
--- /dev/null
+++ b/cryptographic_estimators/LEEstimator/le_helper.py
@@ -0,0 +1,11 @@
+from math import log2, inf, \
+ comb as binomial
+
+
+def cost_to_find_random_2dim_subcodes_with_support_w(n: int, k: int, w: int):
+ """
+ returns the cost of finding a 2 dimensional subcode in a code of length n and dimension k and support w
+ """
+ if n-k.
+# ****************************************************************************
+
+
+from ..base_problem import BaseProblem
+from .le_constants import *
+from math import log2, factorial
+
+
+class LEProblem(BaseProblem):
+ """
+ Construct an instance of the Linear (Code) Equivalence Problem
+
+ INPUT:
+
+ - ``n`` -- code length
+ - ``k`` -- code dimension
+ - ``q`` -- field size
+ - ``nsolutions`` -- number of (expected) solutions of the problem in logarithmic scale
+ - ``memory_bound`` -- maximum allowed memory to use for solving the problem
+
+ """
+
+ def __init__(self, n: int, k: int, q: int, **kwargs):
+ super().__init__(**kwargs)
+ self.parameters[LE_CODE_LENGTH] = n
+ self.parameters[LE_CODE_DIMENSION] = k
+ self.parameters[LE_FIELD_SIZE] = q
+ self.nsolutions = kwargs.get("nsolutions", max(self.expected_number_solutions(), 0))
+
+ def to_bitcomplexity_time(self, basic_operations: float):
+ """
+ Returns the bit-complexity corresponding to basic_operations Fq additions
+
+ INPUT:
+
+ - ``basic_operations`` -- Number of field additions (logarithmic)
+
+ """
+ _, _, q = self.get_parameters()
+ return basic_operations + log2(log2(q))
+
+ def to_bitcomplexity_memory(self, elements_to_store: float):
+ """
+ Returns the memory bit-complexity associated to a given number of Fq elements to store
+
+ INPUT:
+
+ - ``elements_to_store`` -- number of elements to store (logarithmic)
+
+ """
+ _, _, q = self.get_parameters()
+ return elements_to_store + log2(log2(q))
+
+ def expected_number_solutions(self):
+ """
+ Returns the logarithm of the expected number of existing solutions to the problem
+
+ """
+ n, k, q = self.get_parameters()
+ return log2(q) * k * k + log2(factorial(n)) - log2(q) * n * (k - 1)
+
+ def __repr__(self):
+ n, k, q = self.get_parameters()
+ rep = "permutation equivalence problem with (n,k,q) = " \
+ + "(" + str(n) + "," + str(k) + "," + str(q) + ")"
+
+ return rep
+
+ def get_parameters(self):
+ """
+ Returns n, k, q
+ """
+ return self.parameters.values()
diff --git a/cryptographic_estimators/MQEstimator/MQAlgorithms/__init__.py b/cryptographic_estimators/MQEstimator/MQAlgorithms/__init__.py
new file mode 100644
index 00000000..0a6966bf
--- /dev/null
+++ b/cryptographic_estimators/MQEstimator/MQAlgorithms/__init__.py
@@ -0,0 +1,12 @@
+from .bjorklund import Bjorklund
+from .boolean_solve_fxl import BooleanSolveFXL
+from .cgmta import CGMTA
+from .crossbred import Crossbred
+from .dinur1 import DinurFirst
+from .dinur2 import DinurSecond
+from .exhaustive_search import ExhaustiveSearch
+from .f5 import F5
+from .hybrid_f5 import HybridF5
+from .kpg import KPG
+from .lokshtanov import Lokshtanov
+from .mht import MHT
\ No newline at end of file
diff --git a/cryptographic_estimators/MQEstimator/MQAlgorithms/bjorklund.py b/cryptographic_estimators/MQEstimator/MQAlgorithms/bjorklund.py
new file mode 100644
index 00000000..ec8f632c
--- /dev/null
+++ b/cryptographic_estimators/MQEstimator/MQAlgorithms/bjorklund.py
@@ -0,0 +1,201 @@
+# ****************************************************************************
+# 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 ...MQEstimator.mq_algorithm import MQAlgorithm
+from ...MQEstimator.mq_problem import MQProblem
+from ...MQEstimator.mq_helper import sum_of_binomial_coefficients
+from ...base_algorithm import optimal_parameter
+from math import log2
+from sage.functions.other import floor, ceil
+
+
+class Bjorklund(MQAlgorithm):
+ r"""
+ Construct an instance of Bjorklund et al.'s estimator
+
+ Bjorklund et al.'s is a probabilistic algorithm to solve the MQ problem of GF(2) [BKW19]_. It finds a solution of a qudractic
+ system by computing the parity of it number of solutions.
+
+ INPUT:
+
+ - ``problem`` --MQProblem object including all necessary parameters
+ - ``h`` -- external hybridization parameter (default: 0)
+ - ``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)
+ - ``complexity_type`` -- complexity type to consider (0: estimate, 1: tilde O complexity, default: 0)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.bjorklund import Bjorklund
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Bjorklund(MQProblem(n=10, m=12, q=2))
+ sage: E
+ BjΓΆrklund et al. estimator for the MQ problem with 10 variables and 12 polynomials
+
+ """
+
+ def __init__(self, problem: MQProblem, **kwargs):
+ if problem.order_of_the_field() != 2:
+ raise TypeError("q must be equal to 2")
+ super().__init__(problem, **kwargs)
+ self._name = "BjΓΆrklund et al."
+ self._k = floor(log2(2 ** self.problem.nsolutions + 1))
+ n, m, _ = self.get_reduced_parameters()
+
+ self.set_parameter_ranges('lambda_', 3 / n, min(m, n - 1)/n)
+
+ @optimal_parameter
+ def lambda_(self):
+ """
+ Return the optimal lambda_
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.bjorklund import Bjorklund
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Bjorklund(MQProblem(n=10, m=12, q=2))
+ sage: E.lambda_()
+ 3/10
+ """
+ return self._get_optimal_parameter('lambda_')
+
+ def _valid_choices(self):
+ n, _, _ = self.get_reduced_parameters()
+ ranges = self._parameter_ranges
+ l_min = max(3, ceil(ranges['lambda_']['min'] * n))
+ l_max = min(ceil(ranges['lambda_']['max'] * n), n)
+ l = l_min
+ stop = False
+ while not stop:
+ temp_lambda = l / n
+ yield {'lambda_': temp_lambda}
+ l += 1
+ if l > l_max:
+ stop = True
+
+ def _compute_time_complexity(self, parameters: dict):
+ """
+ Return the time complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.bjorklund import Bjorklund
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Bjorklund(MQProblem(n=10, m=12, q=2), bit_complexities=False)
+ sage: E.time_complexity(lambda_=7/10)
+ 49.97565549640329
+ """
+ lambda_ = parameters['lambda_']
+ n, m, _ = self.get_reduced_parameters()
+ k = self._k
+ h = self._h
+ time = 8 * k * log2(n) * sum([Bjorklund._internal_time_complexity_(
+ n - i, m + k + 2, lambda_) for i in range(1, n)])
+ return h + log2(time)
+
+ def _compute_memory_complexity(self, parameters: dict):
+ """
+ Return the memory complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.bjorklund import Bjorklund
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Bjorklund(MQProblem(n=10, m=12, q=2), bit_complexities=False)
+ sage: E.memory_complexity(lambda_=7/10)
+ 10.225233514599497
+ """
+ lambda_ = parameters['lambda_']
+
+ def _internal_memory_complexity_(_n, _m, _lambda):
+ if _n <= 1:
+ return 0
+ else:
+ s = 48 * _n + 1
+ l = floor(_lambda * _n)
+ return _internal_memory_complexity_(l, l + 2, _lambda) + 2 ** (_n - l) * log2(s) + _m * sum_of_binomial_coefficients(_n, 2)
+
+ n, m, _ = self.get_reduced_parameters()
+ return log2(_internal_memory_complexity_(n, m, lambda_))
+
+ def _compute_tilde_o_time_complexity(self, parameters: dict):
+ """
+ Return the Ε time complexity of Bjorklund et al.'s algorithm
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.bjorklund import Bjorklund
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Bjorklund(MQProblem(n=10, m=12, q=2), complexity_type=1)
+ sage: E.time_complexity(lambda_=7/10)
+ 8.03225
+ """
+ n = self.nvariables_reduced()
+ h = self._h
+ return h + 0.803225 * n
+
+ def _compute_tilde_o_memory_complexity(self, parameters: dict):
+ """
+ Return the Ε time complexity of Bjorklund et al.'s algorithm
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.bjorklund import Bjorklund
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Bjorklund(MQProblem(n=10, m=12, q=2), complexity_type=1)
+ sage: E.memory_complexity(lambda_=7/10)
+ 3
+ """
+ n = self.nvariables_reduced()
+ lambda_ = parameters['lambda_']
+ return (1 - lambda_) * n
+
+ def _find_optimal_tilde_o_parameters(self):
+ """
+ Return the Ε time complexity of Bjorklund et al.'s algorithm
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.bjorklund import Bjorklund
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Bjorklund(MQProblem(n=10, m=12, q=2), complexity_type=1)
+ sage: E.optimal_parameters()
+ {'lambda_': 0.19677}
+ """
+ self._optimal_parameters['lambda_'] = 0.19677
+
+ @staticmethod
+ def _internal_time_complexity_(n: int, m: int, lambda_: float):
+ """
+ Helper function. Computes the runtime of the algorithm for given n, m and lambda
+ """
+ if n <= 1:
+ return 1
+ else:
+ l = floor(lambda_ * n)
+ T1 = (n + (l + 2) * m * sum_of_binomial_coefficients(n, 2) +
+ (n - l) * 2 ** (n - l))
+ s = 48 * n + 1
+ return s * sum_of_binomial_coefficients(n - l, l + 4) * (Bjorklund._internal_time_complexity_(l, l + 2, lambda_) + T1)
diff --git a/cryptographic_estimators/MQEstimator/MQAlgorithms/boolean_solve_fxl.py b/cryptographic_estimators/MQEstimator/MQAlgorithms/boolean_solve_fxl.py
new file mode 100644
index 00000000..8833d510
--- /dev/null
+++ b/cryptographic_estimators/MQEstimator/MQAlgorithms/boolean_solve_fxl.py
@@ -0,0 +1,270 @@
+# ****************************************************************************
+# 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 ...MQEstimator.mq_algorithm import MQAlgorithm
+from ...MQEstimator.mq_problem import MQProblem
+from ...MQEstimator import witness_degree
+from ...base_algorithm import optimal_parameter
+from math import log2
+from sage.all import Integer
+from sage.rings.infinity import Infinity
+from sage.arith.misc import binomial
+from ..mq_constants import *
+
+
+class BooleanSolveFXL(MQAlgorithm):
+ """
+ Construct an instance of BooleanSolve and FXL estimator
+
+ BooleanSolve and FXL are algorithms to solve the MQ problem over GF(2) and GF(q), respectively [BFSS11]_ [CKPS]_.
+ They work by guessing the value of $k$ variables and computing the consistency of the resulting subsystem.
+
+ INPUT:
+
+ - ``problem`` -- MQProblem object including all necessary parameters
+ - ``w`` -- linear algebra constant (2 <= w <= 3) (default: 2)
+ - ``h`` -- external hybridization parameter (default: 0)
+ - ``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)
+ - ``complexity_type`` -- complexity type to consider (0: estimate, 1: tilde O complexity, default: 0)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.boolean_solve_fxl import BooleanSolveFXL
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = BooleanSolveFXL(MQProblem(n=10, m=12, q=7))
+ sage: E
+ BooleanSolveFXL estimator for the MQ problem with 10 variables and 12 polynomials
+ """
+ _variants = (MQ_LAS_VEGAS, MQ_DETERMINISTIC)
+
+ def __init__(self, problem: MQProblem, **kwargs):
+ q = problem.order_of_the_field()
+ if not isinstance(q, (int, Integer)):
+ raise TypeError("q must be an integer")
+ super(BooleanSolveFXL, self).__init__(problem, **kwargs)
+ n, m, _ = self.get_reduced_parameters()
+ self._name = "BooleanSolveFXL"
+ if self.problem.is_defined_over_finite_field():
+ if m < n and m != n:
+ raise ValueError(
+ "the no. of polynomials must be >= than the no. of variables")
+ else:
+ if m < n:
+ raise ValueError(
+ "the no. of polynomials must be > than the no. of variables")
+
+ a = 0 if self.problem.is_overdefined_system() else 1
+ self.set_parameter_ranges('k', a, max(n - 1, 1))
+
+ @optimal_parameter
+ def k(self):
+ """
+ Return the optimal `k`
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.boolean_solve_fxl import BooleanSolveFXL
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = BooleanSolveFXL(MQProblem(n=10, m=12, q=7))
+ sage: E.k()
+ 4
+ """
+ return self._get_optimal_parameter('k')
+
+ @optimal_parameter
+ def variant(self):
+ """
+ Return the optimal variant
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.boolean_solve_fxl import BooleanSolveFXL
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = BooleanSolveFXL(MQProblem(n=10, m=12, q=7))
+ sage: E.variant()
+ 'deterministic'
+ """
+ return self._get_optimal_parameter(MQ_VARIANT)
+
+ def _valid_choices(self):
+ """
+ Generator which yields on each call a new set of valid parameters for the optimization routine based.
+ """
+ new_ranges = self. _fix_ranges_for_already_set_parameters()
+ _ = new_ranges.pop(MQ_VARIANT)
+
+ variant = MQ_LAS_VEGAS
+ indices = {i: new_ranges[i]["min"] for i in new_ranges}
+ stop = False
+ while not stop:
+ aux = indices.copy()
+ aux.update({MQ_VARIANT: variant})
+ yield aux
+ indices['k'] += 1
+ if indices['k'] > new_ranges['k']["max"] and variant != MQ_DETERMINISTIC:
+ indices['k'] = new_ranges['k']["min"]
+ variant = MQ_DETERMINISTIC
+ elif indices['k'] > new_ranges['k']["max"] and variant == MQ_DETERMINISTIC:
+ stop = True
+
+ def _compute_time_complexity(self, parameters: dict):
+ """
+ Return the time complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.boolean_solve_fxl import BooleanSolveFXL
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = BooleanSolveFXL(MQProblem(n=10, m=12, q=7), bit_complexities=False)
+ sage: E.time_complexity(k=2, variant = 'las_vegas')
+ 33.35111811760744
+ """
+ k = parameters['k']
+ variant = parameters[MQ_VARIANT]
+ n, m, q = self.get_reduced_parameters()
+ w = self.linear_algebra_constant()
+
+ wit_deg = witness_degree.quadratic_system(n=n - k, m=m, q=q)
+
+ if variant == MQ_LAS_VEGAS:
+ time_complexity = 3 * \
+ binomial(n - k + 2, 2) * q ** k * \
+ binomial(n - k + wit_deg, wit_deg) ** 2
+ elif variant == MQ_DETERMINISTIC:
+ time_complexity = q ** k * m * \
+ binomial(n - k + wit_deg, wit_deg) ** w
+ else:
+ raise ValueError(
+ "variant must either be las_vegas or deterministic")
+
+ h = self._h
+ return log2(time_complexity) + h * log2(q)
+
+ def _compute_memory_complexity(self, parameters: dict):
+ """
+ Return the memory complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.boolean_solve_fxl import BooleanSolveFXL
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = BooleanSolveFXL(MQProblem(n=10, m=12, q=7), bit_complexities=False)
+ sage: E.memory_complexity(k=2, variant='las_vegas')
+ 16.26373284384231
+
+ sage: E.memory_complexity()
+ 11.614709844115207
+ """
+ k = parameters['k']
+ variant = parameters[MQ_VARIANT]
+ n, m, q = self.get_reduced_parameters()
+
+ wit_deg = witness_degree.quadratic_system(n=n - k, m=m, q=q)
+ if variant == MQ_LAS_VEGAS:
+ a = binomial(n - k + 2, 2)
+ T = binomial(n - k + wit_deg - 2, wit_deg)
+ N = binomial(n - k + wit_deg, wit_deg)
+ memory_complexity = max(m * a + \
+ (T * a * log2(N) + N * log2(m)) / log2(q), m * n ** 2)
+ elif variant == MQ_DETERMINISTIC:
+ memory_complexity = max(
+ binomial(n - k + wit_deg - 1, wit_deg) ** 2, m * n ** 2)
+ else:
+ raise ValueError(
+ "variant must either be las_vegas or deterministic")
+
+ return log2(memory_complexity)
+
+ def _compute_tilde_o_time_complexity(self, parameters: dict):
+ """
+ Return the Ε time complexity of BooleanSolve and FXL algorithms
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.boolean_solve_fxl import BooleanSolveFXL
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = BooleanSolveFXL(MQProblem(n=10, m=12, q=7), complexity_type=1)
+ sage: E.time_complexity(k=2, variant='las_vegas')
+ 26.274302520556613
+
+ sage: E.time_complexity()
+ 24.014054533787938
+ """
+ n, m, q = self.get_reduced_parameters()
+ w = self.linear_algebra_constant()
+ k = parameters['k']
+ variant = parameters[MQ_VARIANT]
+ wit_deg = witness_degree.quadratic_system(n=n - k, m=m, q=q)
+
+ if n == m and q == 2:
+ return 0.792 * m
+ elif variant == MQ_LAS_VEGAS:
+ complexity = log2(q ** k * binomial(n - k + wit_deg, wit_deg) ** 2)
+ else:
+ complexity = log2(q ** k * binomial(n - k + wit_deg, wit_deg) ** w)
+
+ complexity += self._h * log2(q)
+ return complexity
+
+ def _compute_tilde_o_memory_complexity(self, parameters: dict):
+ """
+ Return the Ε time complexity of BooleanSolve and FXL algorithms
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.boolean_solve_fxl import BooleanSolveFXL
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = BooleanSolveFXL(MQProblem(n=10, m=12, q=7), complexity_type=1)
+ sage: E.memory_complexity(k=2, variant='las_vegas')
+ 20.659592676441402
+ """
+ n, m, q = self.get_reduced_parameters()
+ k = parameters['k']
+ wit_deg = witness_degree.quadratic_system(n=n - k, m=m, q=q)
+ memory = max(log2(binomial(n - k + wit_deg, wit_deg)) * 2, log2(m * n ** 2))
+ return memory
+
+ def _find_optimal_tilde_o_parameters(self):
+ """
+ Finds the optimal parameters.
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.boolean_solve_fxl import BooleanSolveFXL
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = BooleanSolveFXL(MQProblem(n=10, m=12, q=7), complexity_type=1)
+ sage: E.optimal_parameters()
+ {'k': 4, 'variant': 'deterministic'}
+ """
+ self._find_optimal_parameters()
diff --git a/cryptographic_estimators/MQEstimator/MQAlgorithms/cgmta.py b/cryptographic_estimators/MQEstimator/MQAlgorithms/cgmta.py
new file mode 100644
index 00000000..981062a0
--- /dev/null
+++ b/cryptographic_estimators/MQEstimator/MQAlgorithms/cgmta.py
@@ -0,0 +1,152 @@
+# ****************************************************************************
+# 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 ...MQEstimator.mq_algorithm import MQAlgorithm
+from ...MQEstimator.mq_problem import MQProblem
+from ...helper import ComplexityType
+from math import log2
+from sage.all import Integer
+from sage.functions.other import sqrt, floor
+from sage.functions.other import binomial
+
+
+class CGMTA(MQAlgorithm):
+ r"""
+ Construct an instance of CGMT-A estimator
+
+ CGMT-A is an algorithm to solve the MQ problem over any finite field. It works when there is an integer $k$ such
+ that $m - 2k < 2k^2 \leq n - 2k$ [CGMT02]_.
+
+ NOTE::
+
+ In this module the compleixties are computed
+ for k= min(m / 2, floor(sqrt(n / 2 - sqrt(n / 2)))).
+
+
+ INPUT:
+
+ - ``problem`` -- MQProblem object including all necessary parameters
+ - ``h`` -- external hybridization parameter (default: 0)
+ - ``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)
+ - ``complexity_type`` -- complexity type to consider (0: estimate, 1: tilde O complexity, default: 0)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.cgmta import CGMTA
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = CGMTA(MQProblem(n=41, m=10, q=3))
+ sage: E
+ CGMT-A estimator for the MQ problem with 41 variables and 10 polynomials
+
+ TESTS::
+
+ sage: E.problem.nvariables() == E.nvariables_reduced()
+ True
+
+ """
+
+ def __init__(self, problem: MQProblem, **kwargs):
+ n, m, q = problem.get_problem_parameters()
+ if not isinstance(q, (int, Integer)):
+ raise TypeError("q must be an integer")
+
+ if m > n:
+ raise ValueError("m must be <= n")
+
+ super().__init__(problem, **kwargs)
+ self._k = min(m / 2, floor(sqrt(n / 2 - sqrt(n / 2))))
+
+ if 2 * self._k ** 2 > n - 2 * self._k or m - 2 * self._k >= 2 * self._k ** 2:
+ raise ValueError(
+ f'The condition m - 2k < 2k^2 <= n - 2k must be satisfied')
+
+ self._name = "CGMT-A"
+ self._n_reduced = n
+ self._m_reduced = m
+
+ def _compute_time_complexity(self, parameters: dict):
+ """
+ Return the time complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.cgmta import CGMTA
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = CGMTA(MQProblem(n=41, m=10, q=3), bit_complexities=False)
+ sage: E.time_complexity()
+ 23.137080884841787
+
+ """
+ n, m, q = self.problem.get_problem_parameters()
+ k = self._k
+ time = (m - k) * log2(q)
+ time += log2(2 * k * binomial(n - k, 2))
+ return time
+
+ def _compute_memory_complexity(self, parameters: dict):
+ """
+ Return the memory complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.cgmta import CGMTA
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = CGMTA(MQProblem(n=41, m=10, q=3), bit_complexities=False)
+ sage: E.memory_complexity()
+ 7.339850002884624
+ """
+ q = self.problem.order_of_the_field()
+ k = self._k
+ memory = k * log2(q)
+ memory += log2(2 * k)
+ return memory
+
+ def _compute_tilde_o_time_complexity(self, parameters: dict):
+ """
+ Return the Ε time complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+ """
+ _, m, q = self.problem.get_problem_parameters()
+ k = self._k
+ return (m - k) * log2(q)
+
+ def _compute_tilde_o_memory_complexity(self, parameters: dict):
+ """
+ Return the Ε memory complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+ """
+ q = self.problem.order_of_the_field()
+ k = self._k
+ return k * log2(q)
+
+
diff --git a/cryptographic_estimators/MQEstimator/MQAlgorithms/crossbred.py b/cryptographic_estimators/MQEstimator/MQAlgorithms/crossbred.py
new file mode 100644
index 00000000..6ead5ba1
--- /dev/null
+++ b/cryptographic_estimators/MQEstimator/MQAlgorithms/crossbred.py
@@ -0,0 +1,393 @@
+# ****************************************************************************
+# 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 ...MQEstimator.mq_algorithm import MQAlgorithm
+from ...MQEstimator.mq_problem import MQProblem
+from ...MQEstimator.series.hilbert import HilbertSeries
+from ...MQEstimator.series.nmonomial import NMonomialSeries
+from ...MQEstimator.mq_helper import nmonomials_up_to_degree
+from ...base_algorithm import optimal_parameter
+from math import log2
+from sage.all import Integer
+from sage.rings.all import QQ
+from sage.rings.infinity import Infinity
+from sage.rings.power_series_ring import PowerSeriesRing
+from sage.functions.other import binomial
+
+
+class Crossbred(MQAlgorithm):
+ r"""
+ Construct an instance of crossbred estimator
+
+ The Crossbred is an algorithm to solve the MQ problem [JV18]_. This algorithm consists of two steps, named the
+ preprocessing step and the linearization step. In the preprocessing step, we find a set $S$ of degree-$D$
+ polynomials in the ideal generated by the initial set of polynomials. Every specialization of the first $n-k$
+ variables of the polynomials in $S$ results in a set $S'$ of degree-$d$ polynomials in $k$ variables. Finally, in
+ the linearization step, a solution to $S'$ is found by direct linearization.
+
+ .. NOTE::
+
+ Our complexity estimates are a generalization over any field of size `q` of the complexity formulas given in
+ [Dua20]_, which are given either for `q=2` or generic fields.
+
+ INPUT:
+
+ - ``problem`` -- MQProblem object including all necessary parameters
+ - ``h`` -- external hybridization parameter (default: 0)
+ - ``w`` -- linear algebra constant (2 <= w <= 3) (default: 2)
+ - ``max_D`` -- upper bound to the parameter D (default: 20)
+ - ``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)
+ - ``complexity_type`` -- complexity type to consider (0: estimate, 1: tilde O complexity, default: 0)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.crossbred import Crossbred
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Crossbred(MQProblem(n=10, m=12, q=5))
+ sage: E
+ Crossbred estimator for the MQ problem with 10 variables and 12 polynomials
+ """
+
+ def __init__(self, problem: MQProblem, **kwargs):
+ q = problem.order_of_the_field()
+ if not isinstance(q, (int, Integer)):
+ raise TypeError("q must be an integer")
+
+ super(Crossbred, self).__init__(problem, **kwargs)
+ self._name = "Crossbred"
+ self._max_D = kwargs.get('max_D', min(
+ 30, min(problem.nvariables(), problem.npolynomials())))
+ if not isinstance(self._max_D, (int, Integer)):
+ raise TypeError("max_D must be an integer")
+
+ n = self.nvariables_reduced()
+ self.set_parameter_ranges('k', 1, n)
+ self.set_parameter_ranges('D', 2, self._max_D)
+ self.set_parameter_ranges('d', 1, n)
+
+ @optimal_parameter
+ def k(self):
+ """
+ Return the optimal `k`, i.e. no. of variables in the resulting system
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.crossbred import Crossbred
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Crossbred(MQProblem(n=10, m=12, q=5))
+ sage: E.k()
+ 7
+ """
+ return self._get_optimal_parameter('k')
+
+ @optimal_parameter
+ def D(self):
+ """
+ Return the optimal `D`, i.e. degree of the initial Macaulay matrix
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.crossbred import Crossbred
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Crossbred(MQProblem(n=10, m=12, q=5), max_D = 10)
+ sage: E.D()
+ 5
+ """
+ return self._get_optimal_parameter('D')
+
+ @optimal_parameter
+ def d(self):
+ """
+ Return the optimal `d`, i.e. degree resulting Macaulay matrix
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.crossbred import Crossbred
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Crossbred(MQProblem(n=10, m=12, q=5), max_D = 10)
+ sage: E.d()
+ 1
+ """
+ return self._get_optimal_parameter('d')
+
+ @property
+ def max_D(self):
+ """
+ Return the upper bound of the degree of the initial Macaulay matrix
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.crossbred import Crossbred
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Crossbred(MQProblem(n=10, m=12, q=5))
+ sage: E.max_D
+ 10
+ """
+ return self._max_D
+
+ @max_D.setter
+ def max_D(self, value: int):
+ """
+ Set new upper bound of the degree of the initial Macaulay matrix
+
+ INPUT:
+
+ - ``value`` -- integer to be set as the upper bound of the parameter `D`
+ """
+ self.reset()
+ min_D = self._parameter_ranges['D']['min']
+ self._max_D = value
+ self.set_parameter_ranges('D', min_D, value)
+
+ def _ncols_in_preprocessing_step(self, k: int, D: int, d: int):
+ """
+ Return the number of columns involve in the preprocessing step
+
+ INPUT:
+
+ - ``k`` -- no. variables in the resulting system
+ - ``D`` -- degree of the initial Macaulay matrix
+ - ``d`` -- degree resulting Macaulay matrix
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.crossbred import Crossbred
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Crossbred(MQProblem(n=10, m=12, q=5))
+ sage: E._ncols_in_preprocessing_step(4, 6, 3)
+ 297
+ """
+ if d >= D:
+ raise ValueError("d must be smaller than D")
+
+ n, _, q = self.get_reduced_parameters()
+ nms0 = NMonomialSeries(n=k, q=q, max_prec=D + 1)
+ nms1 = NMonomialSeries(n=n - k, q=q, max_prec=D + 1)
+
+ ncols = 0
+ for dk in range(d + 1, D):
+ ncols += sum([nms0.nmonomials_of_degree(dk) *
+ nms1.nmonomials_of_degree(dp) for dp in range(D - dk)])
+
+ return ncols
+
+ def _ncols_in_linearization_step(self, k: int, d: int):
+ """
+ Return the number of columns involve in the linearization step
+
+ INPUT:
+
+ - ``k`` -- no. variables in the resulting system
+ - ``d`` -- degree resulting Macaulay matrix
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.crossbred import Crossbred
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Crossbred(MQProblem(n=10, m=12, q=5))
+ sage: E._ncols_in_linearization_step(4, 3)
+ 35
+ """
+ return nmonomials_up_to_degree(d, k, q=self.problem.order_of_the_field())
+
+ def _admissible_parameter_series(self, k: int):
+ """
+ Return the series $S_k$ of admissible parameters
+
+ INPUT:
+
+ - ``k`` -- no. variables in the resulting system
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.crossbred import Crossbred
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Crossbred(MQProblem(n=10, m=12, q=5), max_D = 2)
+ sage: E._admissible_parameter_series(2)
+ -1 - 3*x - 3*y - 10*x^2 - 3*x*y + 6*y^2 + O(x, y)^3
+ """
+ n, m, q = self.get_reduced_parameters()
+ max_D = self.max_D
+
+ R = PowerSeriesRing(QQ, names=['x', 'y'], default_prec=max_D + 1)
+ x, y = R.gens()
+
+ Hk = HilbertSeries(n=k, degrees=[2] * m, q=q)
+ k_y, k_xy = Hk.series(y), Hk.series(x * y)
+
+ Hn = HilbertSeries(n=n, degrees=[2] * m, q=q)
+ n_x = Hn.series(x)
+
+ N = NMonomialSeries(n=n - k, q=q, max_prec=max_D + 1)
+ nk_x = N.series_monomials_of_degree()(x)
+
+ return (k_xy * nk_x - n_x - k_y) / ((1 - x) * (1 - y))
+
+ def _valid_choices(self):
+ """
+ Return a list of admissible parameters `(k, D, d)`
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.crossbred import Crossbred
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Crossbred(MQProblem(n=10, m=12, q=5))
+ sage: [list(x.values()) for x in E._valid_choices()][:5] == [[2, 1, 1], [3, 1, 1], [4, 1, 1], [3, 2, 1], [5, 1, 1]]
+ True
+ """
+
+ new_ranges = self._fix_ranges_for_already_set_parameters()
+
+ k = 1
+ stop = False
+ while not stop:
+ Sk = self._admissible_parameter_series(k)
+ for (monomial, coefficient) in Sk.coefficients().items():
+ D, d = monomial.exponents()[0]
+ if 0 <= coefficient and d < D and new_ranges['D']["min"] <= D <= new_ranges['D']["max"] and new_ranges['d']["min"] <= d <= new_ranges['d']["max"]:
+ yield {'D': D, 'd': d, 'k': k}
+
+ k += 1
+ if k > new_ranges['k']["max"]:
+ stop = True
+
+ def _compute_time_complexity(self, parameters: dict):
+ """
+ Return the time complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.crossbred import Crossbred
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Crossbred(MQProblem(n=10, m=12, q=5), bit_complexities=False)
+ sage: E.time_complexity(k=4, D=6, d=4)
+ 29.77510134996699
+
+ sage: E = Crossbred(MQProblem(n=10, m=12, q=5), bit_complexities=False)
+ sage: E.time_complexity()
+ 19.56992234329735
+ """
+ k = parameters['k']
+ D = parameters['D']
+ d = parameters['d']
+ n, m, q = self.get_reduced_parameters()
+ w = self.linear_algebra_constant()
+ np = self._ncols_in_preprocessing_step(k=k, D=D, d=d)
+ nl = self._ncols_in_linearization_step(k=k, d=d)
+ complexity = Infinity
+ if np > 1 and log2(np) > 1:
+ complexity_wiedemann = 3 * binomial(k + d, d) * binomial(n + 2, 2) * np ** 2
+ complexity_gaussian = np ** w
+ complexity_prep = min(complexity_gaussian, complexity_wiedemann)
+ complexity = log2(complexity_prep + m * q ** (n - k) * nl ** w)
+ h = self._h
+ return h * log2(q) + complexity
+
+ def _compute_memory_complexity(self, parameters: dict):
+ """
+ Return the memory complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.crossbred import Crossbred
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Crossbred(MQProblem(n=10, m=12, q=5),bit_complexities=False)
+ sage: E.memory_complexity(k=4, D=6, d=4)
+ 12.892542816648552
+
+ sage: E = Crossbred(MQProblem(n=10, m=12, q=5), bit_complexities=False)
+ sage: E.memory_complexity()
+ 19.38013126659691
+ """
+ k = parameters['k']
+ D = parameters['D']
+ d = parameters['d']
+ ncols_pre_step = self._ncols_in_preprocessing_step(k, D, d)
+ ncols_lin_step = self._ncols_in_linearization_step(k, d)
+ return log2(ncols_pre_step ** 2 + ncols_lin_step ** 2)
+
+ def _compute_tilde_o_time_complexity(self, parameters: dict):
+ """
+ Return the Ε time complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.crossbred import Crossbred
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Crossbred(MQProblem(n=10, m=12, q=5), complexity_type=1)
+ sage: E.time_complexity(k=4, D=6, d=4)
+ 26.190185554770082
+
+ sage: E = Crossbred(MQProblem(n=10, m=12, q=5), complexity_type=1)
+ sage: E.time_complexity()
+ 19.39681379895914
+ """
+ k = parameters['k']
+ D = parameters['D']
+ d = parameters['d']
+ np = self._ncols_in_preprocessing_step(k=k, D=D, d=d)
+ nl = self._ncols_in_linearization_step(k=k, d=d)
+ n, _, q = self.get_reduced_parameters()
+ w = self.linear_algebra_constant()
+ h = self._h
+ return h * log2(q) + log2(np ** 2 + q ** (n - k) * nl ** w)
+
+ def _compute_tilde_o_memory_complexity(self, parameters: dict):
+ """
+ Return the Ε memory complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.crossbred import Crossbred
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Crossbred(MQProblem(n=10, m=12, q=5), complexity_type=1)
+ sage: E.memory_complexity(k=4, D=6, d=4)
+ 12.892542816648552
+ """
+ return self._compute_memory_complexity(parameters)
+
+ def _find_optimal_tilde_o_parameters(self):
+ """
+
+ Return the optimal parameters to achieve the optimal Ε time complexity.
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.crossbred import Crossbred
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Crossbred(MQProblem(n=10, m=12, q=5), complexity_type=1)
+ sage: E.optimal_parameters()
+ {'D': 5, 'd': 1, 'k': 7}
+ """
+ self._find_optimal_parameters()
diff --git a/cryptographic_estimators/MQEstimator/MQAlgorithms/dinur1.py b/cryptographic_estimators/MQEstimator/MQAlgorithms/dinur1.py
new file mode 100644
index 00000000..fcd30a03
--- /dev/null
+++ b/cryptographic_estimators/MQEstimator/MQAlgorithms/dinur1.py
@@ -0,0 +1,254 @@
+# ****************************************************************************
+# 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 ...MQEstimator.mq_helper import sum_of_binomial_coefficients
+from ...MQEstimator.mq_algorithm import MQAlgorithm
+from ...MQEstimator.mq_problem import MQProblem
+from ...helper import ComplexityType
+from math import log2
+from sage.functions.other import floor
+
+
+class DinurFirst(MQAlgorithm):
+ r"""
+ Construct an instance of Dinur's first estimator
+
+ The Dinur's first is a probabilistic algorithm to solve the MQ problem over GF(2) [Din21a]_. It computes the parity
+ of the number of solutions of many quadratic polynomial systems. These systems come from the specialization, in the
+ original system, of the values in a fixed set of variables.
+
+
+ INPUT:
+
+ - ``problem`` -- MQProblem object including all necessary parameters
+ - ``nsolutions`` -- number of solutions (default: 1)
+ - ``h`` -- external hybridization parameter (default: 0)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.dinur1 import DinurFirst
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = DinurFirst(MQProblem(n=10, m=12, q=2))
+ sage: E
+ Dinur1 estimator for the MQ problem with 10 variables and 12 polynomials
+ """
+
+ def __init__(self, problem: MQProblem, **kwargs):
+ if problem.order_of_the_field() != 2:
+ raise TypeError("q must be equal to 2")
+ super().__init__(problem, **kwargs)
+ self._name = "Dinur1"
+ self._k = floor(log2(2 ** self.problem.nsolutions + 1))
+ n, m, _ = self.get_reduced_parameters()
+ self.set_parameter_ranges('kappa', 1 / n, 1/3)
+ self.set_parameter_ranges('lambda_', 1 / (n - 1), 0.999)
+
+ @optimal_parameter
+ def lambda_(self):
+ """
+ Return the optimal lambda_
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.dinur1 import DinurFirst
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = DinurFirst(MQProblem(n=10, m=12, q=2))
+ sage: E.lambda_()
+ 2/9
+ """
+ return self._get_optimal_parameter('lambda_')
+
+ @optimal_parameter
+ def kappa(self):
+ """
+ Return the optimal kappa
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.dinur1 import DinurFirst
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = DinurFirst(MQProblem(n=10, m=12, q=2))
+ sage: E.kappa()
+ 1/3
+ """
+ return self._get_optimal_parameter('kappa')
+
+ def _valid_choices(self):
+ """
+ Generator which yields on each call a new set of valid parameters for the optimization routine based.
+
+ """
+ new_ranges = self._fix_ranges_for_already_set_parameters()
+
+ n, m, _ = self.get_reduced_parameters()
+ n1_min = max(2, floor(new_ranges['kappa']['min'] * (n - 1)))
+ n1_max = min(floor(new_ranges['kappa']
+ ['max'] * (n - 1)), (n - 1) // 3 + 1)
+ n1 = n1_min
+ n2 = 1
+ kappa = n1 / (n - 1)
+ lambda_ = (n1 - n2) / (n - 1)
+ stop = False
+ while not stop:
+ yield {'kappa': kappa, 'lambda_': lambda_}
+ n2 += 1
+ if n2 >= n1:
+ n2 = 1
+ n1 += 1
+ if n1 > n1_max:
+ stop = True
+
+ kappa = n1 / (n - 1)
+ lambda_ = (n1 - n2) / (n - 1)
+
+ def _T(self, n: int, n1: int, w: int, lambda_: float):
+ t = 48 * n + 1
+ n2 = floor(n1 - lambda_ * n)
+ l = n2 + 2
+ k = self._k
+ m = self.npolynomials_reduced()
+
+ if n2 <= 0:
+ return n * sum_of_binomial_coefficients(n - n1, w) * 2 ** n1
+ else:
+ temp1 = self._T(n, n2, n2 + 4, lambda_)
+ temp2 = n * \
+ sum_of_binomial_coefficients(n - n1, w) * 2 ** (n1 - n2)
+ temp3 = n * sum_of_binomial_coefficients(n - n2, n2 + 4)
+ temp4 = l * (m + k + 2) * sum_of_binomial_coefficients(n, 2)
+ return t * (temp1 + temp2 + temp3 + temp4)
+
+ def _compute_time_complexity(self, parameters: dict):
+ """
+ Return the time complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.dinur1 import DinurFirst
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = DinurFirst(MQProblem(n=10, m=12, q=2))
+ sage: E.time_complexity(kappa=0.9, lambda_=0.9)
+ 16.73237302312492
+
+ sage: E = DinurFirst(MQProblem(n=10, m=12, q=2), bit_complexities=False)
+ sage: E.time_complexity()
+ 26.81991353901186
+
+ """
+ lambda_ = parameters['lambda_']
+ kappa = parameters['kappa']
+ k = self._k
+ n = self.nvariables_reduced()
+
+ def w(i, kappa):
+ return floor((n - i) * (1 - kappa))
+
+ def n1(i, kappa):
+ return floor((n - i) * kappa)
+
+ time = 8 * k * \
+ log2(n) * sum([self._T(n - i, n1(i, kappa),
+ w(i, kappa), lambda_) for i in range(1, n)])
+ h = self._h
+ return h + log2(time)
+
+ def _compute_memory_complexity(self, parameters: dict):
+ """
+ Return the memory complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.dinur1 import DinurFirst
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = DinurFirst(MQProblem(n=10, m=12, q=2), bit_complexities=False)
+ sage: E.memory_complexity(kappa=0.9, lambda_=0.9)
+ 8.909893083770042
+
+ sage: E = DinurFirst(MQProblem(n=10, m=12, q=2), bit_complexities=False)
+ sage: E.memory_complexity()
+ 14.909893083770042
+
+ """
+ kappa = parameters['kappa']
+ n = self.nvariables_reduced()
+ memory = log2(48 * n + 1) + floor((1 - kappa) * n)
+ return memory
+
+ def _compute_tilde_o_time_complexity(self, parameters: dict):
+ """
+ Return the Ε time complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.dinur1 import DinurFirst
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = DinurFirst(MQProblem(n=10, m=12, q=2), complexity_type=1)
+ sage: E.time_complexity(kappa=0.9, lambda_=0.9)
+ 6.9430000000000005
+
+ sage: E = DinurFirst(MQProblem(n=10, m=12, q=2), complexity_type=1)
+ sage: E.time_complexity()
+ 6.9430000000000005
+ """
+ n = self.nvariables_reduced()
+ h = self._h
+ return h + 0.6943 * n
+
+ def _compute_tilde_o_memory_complexity(self, parameters: dict):
+ """
+ Return the Ε memory complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+ """
+ kappa = parameters['kappa']
+ n = self.nvariables_reduced()
+ memory = (1 - kappa) * n
+ return memory
+
+ def _find_optimal_tilde_o_parameters(self):
+ """
+ Return the Ε time complexity of DinurFirst et al.'s algorithm
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.dinur1 import DinurFirst
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = DinurFirst(MQProblem(n=10, m=12, q=2), complexity_type=1)
+ sage: E.optimal_parameters()
+ {'kappa': 0.3057, 'lambda_': 0.3010299956639812}
+ """
+ n = self.nvariables_reduced()
+ lambda_ = 1 / log2(n)
+ kappa = 0.3057
+ self._optimal_parameters['kappa'] = kappa
+ self._optimal_parameters['lambda_'] = lambda_
diff --git a/cryptographic_estimators/MQEstimator/MQAlgorithms/dinur2.py b/cryptographic_estimators/MQEstimator/MQAlgorithms/dinur2.py
new file mode 100644
index 00000000..6568993c
--- /dev/null
+++ b/cryptographic_estimators/MQEstimator/MQAlgorithms/dinur2.py
@@ -0,0 +1,189 @@
+# ****************************************************************************
+# 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 ...MQEstimator.mq_algorithm import MQAlgorithm
+from ...MQEstimator.mq_problem import MQProblem
+from ...MQEstimator.mq_helper import sum_of_binomial_coefficients
+from ...base_algorithm import optimal_parameter
+from math import log2
+from sage.rings.infinity import Infinity
+from sage.functions.other import floor
+
+
+class DinurSecond(MQAlgorithm):
+ """
+ Construct an instance of Dinur's second estimator
+
+ Dinur's second is a probabilistic algorithm to solve the MQ problem over GF(2) [Din21b]_. It is based on ideas from
+ [Din21a]_.
+
+ INPUT:
+
+ - ``problem`` -- MQProblem object including all necessary parameters
+ - ``h`` -- external hybridization parameter (default: 0)
+ - ``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)
+ - ``complexity_type`` -- complexity type to consider (0: estimate, 1: tilde O complexity, default: 0)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.dinur2 import DinurSecond
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = DinurSecond(MQProblem(n=10, m=12, q=2))
+ sage: E
+ Dinur2 estimator for the MQ problem with 10 variables and 12 polynomials
+
+ """
+
+ def __init__(self, problem: MQProblem, **kwargs):
+ if problem.order_of_the_field() != 2:
+ raise TypeError("q must be equal to 2")
+ super().__init__(problem, **kwargs)
+
+ self._name = "Dinur2"
+ self._k = floor(log2(2 ** self.problem.nsolutions + 1))
+ n, m, _ = self.get_reduced_parameters()
+ self.set_parameter_ranges('n1', 1, n// 2 - 1)
+
+ @optimal_parameter
+ def n1(self):
+ """
+ Return the optimal parameter $n_1$
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.dinur2 import DinurSecond
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = DinurSecond(MQProblem(n=10, m=12, q=2))
+ sage: E.n1()
+ 4
+ """
+ return self._get_optimal_parameter('n1')
+
+ def _compute_time_complexity(self, parameters: dict):
+ """
+ Return the time complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.dinur2 import DinurSecond
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = DinurSecond(MQProblem(n=10, m=12, q=2), bit_complexities=False)
+ sage: E.time_complexity(n1=4)
+ 15.809629225117881
+
+ sage: E.time_complexity(n1=2, bit_complexities=False)
+ 15.844709299018824
+
+ sage: E = DinurSecond(MQProblem(n=10, m=12, q=2), bit_complexities=False)
+ sage: E.time_complexity()
+ 15.809629225117881
+
+ """
+ n1 = parameters['n1']
+ n = self.nvariables_reduced()
+ time = 16 * log2(n) * 2 ** n1 * sum_of_binomial_coefficients(n - n1, n1 + 3) + \
+ n1 * n * 2 ** (n - n1) + 2 ** (n - 2 * n1 + 1) * \
+ sum_of_binomial_coefficients(n, 2)
+ h = self._h
+ return h + log2(time)
+
+ def _compute_memory_complexity(self, parameters: dict):
+ """
+ Return the memory complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.dinur2 import DinurSecond
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = DinurSecond(MQProblem(n=10, m=12, q=2), bit_complexities=False)
+ sage: E.memory_complexity(n1=4)
+ 11.321928094887362
+
+ sage: E.memory_complexity(n1=2)
+ 12.35974956032233
+
+ sage: E = DinurSecond(MQProblem(n=10, m=12, q=2))
+ sage: E.memory_complexity()
+ 11.321928094887362
+
+ """
+ n = self.nvariables_reduced()
+ n1 = parameters['n1']
+ return log2(8 * (n1 + 1) * sum_of_binomial_coefficients(n - n1, n1 + 3))
+
+ def _compute_tilde_o_time_complexity(self, parameters: dict):
+ """
+ Compute and return the time complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.dinur2 import DinurSecond
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = DinurSecond(MQProblem(n=10, m=12, q=2), complexity_type=1)
+ sage: E.time_complexity(n1=2)
+ 8.148148148148149
+ """
+ n = self.nvariables_reduced()
+ h = self._h
+ return h + ((1 - 1./(2.7*2)) * n)
+
+ def _compute_tilde_o_memory_complexity(self, parameters: dict):
+ """
+ Compute and return the memory complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.dinur2 import DinurSecond
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = DinurSecond(MQProblem(n=10, m=12, q=2), complexity_type=1)
+ sage: E.memory_complexity(n1=2)
+ 6.3
+ """
+ n = self.nvariables_reduced()
+ return n * 0.63
+
+ def _find_optimal_tilde_o_parameters(self):
+ """
+ Return the Ε time complexity of Bjorklund et al.'s algorithm
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.dinur2 import DinurSecond
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = DinurSecond(MQProblem(n=10, m=12, q=2), complexity_type=1)
+ sage: E.optimal_parameters()
+ {'n1': 1.8518518518518516}
+ """
+ n = self.nvariables_reduced()
+ self._optimal_parameters['n1'] = n/(2.7 * 2)
diff --git a/cryptographic_estimators/MQEstimator/MQAlgorithms/exhaustive_search.py b/cryptographic_estimators/MQEstimator/MQAlgorithms/exhaustive_search.py
new file mode 100644
index 00000000..a27b0cfb
--- /dev/null
+++ b/cryptographic_estimators/MQEstimator/MQAlgorithms/exhaustive_search.py
@@ -0,0 +1,126 @@
+# ****************************************************************************
+# 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 ...MQEstimator.mq_algorithm import MQAlgorithm
+from ...MQEstimator.mq_problem import MQProblem
+from ...helper import ComplexityType
+from math import log2
+from sage.all import Integer
+from sage.functions.log import log
+
+
+class ExhaustiveSearch(MQAlgorithm):
+ r"""
+ Construct an instance of Exhaustive Search estimator
+ ExhaustiveSearch solves the MQ problem by evaluating all possible solutions until one is found.
+ The formulas used in this module are generalizations of one shown in [BCCCNSY10]_
+
+ INPUT:
+
+ - ``problem`` -- MQProblem object including all necessary parameters
+ - ``h`` -- external hybridization parameter (default: 0)
+ - ``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)
+ - ``complexity_type`` -- complexity type to consider (0: estimate, 1: tilde O complexity, default: 0)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.exhaustive_search import ExhaustiveSearch
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = ExhaustiveSearch(MQProblem(n=10, m=12, q=3))
+ sage: E
+ ExhaustiveSearch estimator for the MQ problem with 10 variables and 12 polynomials
+ """
+
+ def __init__(self, problem: MQProblem, **kwargs):
+ q = problem.order_of_the_field()
+ if not isinstance(q, (int, Integer)):
+ raise TypeError("q must be an integer")
+
+ super().__init__(problem, **kwargs)
+ self._name = "ExhaustiveSearch"
+
+ def _compute_time_complexity(self, parameters: dict):
+ """
+ Return the time complexity of the algorithm for a given set of parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.exhaustive_search import ExhaustiveSearch
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = ExhaustiveSearch(MQProblem(q=3, n=10, m=12), bit_complexities=False)
+ sage: E.time_complexity()
+ 15.917197145402291
+
+ sage: E0 = ExhaustiveSearch(MQProblem(n=15, m=12, q=3))
+ sage: E1 = ExhaustiveSearch(MQProblem(n=17, m=12, q=3))
+ sage: E0.time_complexity() == E1.time_complexity()
+ True
+ """
+ n, _, q = self.get_reduced_parameters()
+ nsolutions = 2 ** self.problem.nsolutions
+ time = n * log2(q)
+ if q == 2:
+ time += log2(4 * log2(n))
+ else:
+ time += log2(log(n, q))
+ time -= log2(nsolutions + 1)
+ h = self._h
+ time += h * log2(q)
+ return time
+
+ def _compute_memory_complexity(self, parameters: dict):
+ """
+ Return the memory complexity of the algorithm for a given set of parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.exhaustive_search import ExhaustiveSearch
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = ExhaustiveSearch(MQProblem(q=3, n=10, m=12), bit_complexities=False)
+ sage: E.memory_complexity()
+ 10.228818690495881
+
+
+ sage: E0 = ExhaustiveSearch(MQProblem(n=15, m=12, q=3))
+ sage: E1 = ExhaustiveSearch(MQProblem(n=17, m=12, q=3))
+ sage: E0.memory_complexity() == E1.memory_complexity()
+ True
+ """
+ n, m, _ = self.get_reduced_parameters()
+ return log2(m * n ** 2)
+
+
+ def _compute_tilde_o_time_complexity(self, parameters: dict):
+ """
+ Return the Ε time complexity of the algorithm for a given set of parameters
+
+ """
+ n, _, q = self.get_reduced_parameters()
+ return n * log2(q)
+
+ def _compute_tilde_o_memory_complexity(self, parameters: dict):
+ """
+ Return the Ε memory complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ """
+ return 0
+
diff --git a/cryptographic_estimators/MQEstimator/MQAlgorithms/f5.py b/cryptographic_estimators/MQEstimator/MQAlgorithms/f5.py
new file mode 100644
index 00000000..59b75cc9
--- /dev/null
+++ b/cryptographic_estimators/MQEstimator/MQAlgorithms/f5.py
@@ -0,0 +1,260 @@
+# ****************************************************************************
+# 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 ...MQEstimator.mq_algorithm import MQAlgorithm
+from ...MQEstimator.mq_problem import MQProblem
+from ...MQEstimator import degree_of_regularity
+from ...helper import ComplexityType
+from math import log2, inf
+from sage.functions.other import binomial
+
+
+class F5(MQAlgorithm):
+ """
+ Construct an instance of F5 complexity estimator
+
+ INPUT:
+
+ - ``problem`` -- MQProblem object including all necessary parameters
+ - ``h`` -- external hybridization parameter (default: 0)
+ - ``w`` -- linear algebra constant (default: 2)
+ - ``degrees`` -- a list/tuple of degree of the polynomials (default: [2]*m)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.f5 import F5
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = F5(MQProblem(n=10, m=5, q=3))
+ sage: E
+ F5 estimator for the MQ problem with 10 variables and 5 polynomials
+ """
+
+ def __init__(self, problem: MQProblem, **kwargs):
+ m = problem.npolynomials()
+ degrees = kwargs.get('degrees', [2]*m)
+ if len(degrees) != m:
+ raise ValueError(f"len(degrees) must be equal to {m}")
+
+ super().__init__(problem, **kwargs)
+ if degrees == [2]*m:
+ self._degrees = [2]*self.npolynomials_reduced()
+ else:
+ self._degrees = degrees
+
+ self._name = "F5"
+ self._time_complexity = None
+ self._memory_complexity = None
+ self._dreg = None
+
+ def degree_of_polynomials(self):
+ """
+ Return a list of degree of the polynomials
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.f5 import F5
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = F5(MQProblem(n=10, m=5, q=3))
+ sage: E.degree_of_polynomials()
+ [2, 2, 2, 2]
+
+ """
+ return self._degrees
+
+ def _compute_time_complexity(self, parameters: dict):
+ """
+ Return the time complexity of the algorithm for a given set of parameters
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.f5 import F5
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = F5(MQProblem(n=10, m=15, q=3), bit_complexities=False)
+ sage: E.time_complexity()
+ 23.841343113280505
+
+ TESTS::
+
+ sage: F5(MQProblem(n=10, m=12, q=5)).time_complexity()
+ 31.950061609866715
+
+ """
+ if self.problem.is_overdefined_system():
+ time = self._time_complexity_semi_regular_system()
+ else:
+ time = self._time_complexity_regular_system()
+
+ return max(time, self._time_complexity_fglm())
+
+ def _time_complexity_fglm(self):
+ """
+ Return the time complexity of the FGLM algorithm for this system
+
+ TEST::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.f5 import F5
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = F5(MQProblem(n=10, m=15, q=3, nsolutions=1))
+ sage: E._time_complexity_fglm()
+ 6.321928094887363
+ """
+ n, _, q = self.get_reduced_parameters()
+ D = 2 ** self.problem.nsolutions
+ h = self._h
+ return h * log2(q) + log2(n * D ** 3)
+
+ def _time_complexity_regular_system(self):
+ """
+ Return the time complexity for regular system
+
+ TEST::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.f5 import F5
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = F5(MQProblem(n=10, m=5, q=31), bit_complexities=False)
+ sage: E.time_complexity()
+ 15.954559846999834
+ """
+ if not (self.problem.is_square_system() or self.problem.is_underdefined_system()):
+ raise ValueError(
+ "regularity assumption is valid only on square or underdefined system")
+
+ n, m, q = self.get_reduced_parameters()
+ w = self.linear_algebra_constant()
+ if self._dreg is None:
+ self._dreg = degree_of_regularity.quadratic_system(n, m, q)
+ dreg = self._dreg
+ time = w * log2(binomial(n + dreg, dreg))
+ time += log2(m)
+ h = self._h
+ return h * log2(q) + time
+
+ def _time_complexity_semi_regular_system(self):
+ """
+ Return the time complexity for semi-regular system
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.f5 import F5
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: F5_ = F5(MQProblem(n=5, m=10, q=31), bit_complexities=False)
+ sage: F5_.time_complexity()
+ 14.93663793900257
+ """
+ if not self.problem.is_overdefined_system():
+ raise ValueError(
+ "semi regularity assumption is valid only on overdefined system")
+
+ n, m, q = self.get_reduced_parameters()
+ w = self.linear_algebra_constant()
+ if self._dreg is None:
+ self._dreg = degree_of_regularity.quadratic_system(n, m, q)
+ dreg = self._dreg
+ time = w * log2(binomial(n + dreg, dreg))
+ time += log2(m)
+ h = self._h
+ return h * log2(q) + time
+
+ def _compute_memory_complexity(self, parameters: dict):
+ """
+ Return the memory complexity of the algorithm for a given set of parameters
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.f5 import F5
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: F5_ = F5(MQProblem(n=10, m=12, q=5), bit_complexities=False)
+ sage: F5_.memory_complexity()
+ 24.578308707446713
+
+ """
+ n, m, q = self.get_reduced_parameters()
+ if self._dreg is None:
+ self._dreg = degree_of_regularity.quadratic_system(n, m, q)
+ dreg = self._dreg
+ memory = max(log2(binomial(n + dreg - 1, dreg)) * 2, log2(m * n ** 2))
+ return memory
+
+
+ def _compute_tilde_o_time_complexity(self, parameters: dict):
+ """
+ Return the Ε time complexity of the algorithm for a given set of parameters
+
+ """
+ if self.problem.is_overdefined_system():
+ time = self._tilde_o_time_complexity_semi_regular_system(parameters)
+ else:
+ time = self._tilde_o_time_complexity_regular_system(parameters)
+
+ return max(time, self._tilde_o_time_complexity_fglm(parameters))
+
+ def _tilde_o_time_complexity_fglm(self, parameters: dict):
+ """
+ Return the Ε time complexity of the FGLM algorithm for this system
+
+ """
+ _, _, q = self.get_reduced_parameters()
+ D = 2 ** self.problem.nsolutions
+ h = self._h
+ return h * log2(q) + log2(D ** 3)
+
+ def _tilde_o_time_complexity_regular_system(self, parameters: dict):
+ """
+ Return the Ε time complexity for regular system
+
+ """
+ if not (self.problem.is_square_system() or self.problem.is_underdefined_system()):
+ raise ValueError(
+ "regularity assumption is valid only on square or underdefined system")
+
+ n, m, q = self.get_reduced_parameters()
+ w = self.linear_algebra_constant()
+ if self._dreg is None:
+ self._dreg = degree_of_regularity.quadratic_system(n, m, q)
+ dreg = self._dreg
+ time = w * log2(binomial(n + dreg, dreg))
+ h = self._h
+ return h * log2(q) + time
+
+ def _tilde_o_time_complexity_semi_regular_system(self, parameters: dict):
+ """
+ Return the Ε time complexity for semi-regular system
+
+ """
+ if not self.problem.is_overdefined_system():
+ raise ValueError(
+ "semi regularity assumption is valid only on overdefined system")
+
+ n, m, q = self.get_reduced_parameters()
+ w = self.linear_algebra_constant()
+ if self._dreg is None:
+ self._dreg = degree_of_regularity.quadratic_system(n, m, q)
+ dreg = self._dreg
+ h = self._h
+ return h * log2(q) + w * log2(binomial(n + dreg, dreg))
+
+ def _compute_tilde_o_memory_complexity(self, parameters: dict):
+ """
+ Return the Ε memory complexity of the algorithm for a given set of parameters
+
+ """
+ n, m, q = self.get_reduced_parameters()
+ if self._dreg is None:
+ self._dreg = degree_of_regularity.quadratic_system(n, m, q)
+ dreg = self._dreg
+ return log2(binomial(n + dreg - 1, dreg)) * 2
\ No newline at end of file
diff --git a/cryptographic_estimators/MQEstimator/MQAlgorithms/hybrid_f5.py b/cryptographic_estimators/MQEstimator/MQAlgorithms/hybrid_f5.py
new file mode 100644
index 00000000..0a6e270f
--- /dev/null
+++ b/cryptographic_estimators/MQEstimator/MQAlgorithms/hybrid_f5.py
@@ -0,0 +1,226 @@
+# ****************************************************************************
+# 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 ...MQEstimator.mq_algorithm import MQAlgorithm
+from ...MQEstimator.mq_problem import MQProblem
+from ...MQEstimator.MQAlgorithms.f5 import F5
+from ...base_algorithm import optimal_parameter
+from ...helper import ComplexityType
+from math import log2
+from sage.all import Integer
+
+
+class HybridF5(MQAlgorithm):
+ r"""
+ Construct an instance of HybridF5
+
+ HybridF5 is an algorithm to solve systems of polynomials over a finite field proposed in [BFP09]_, [BFP12]_. The
+ algorithm is a tradeoff between exhaustive search and Groebner bases computation. The idea is to fix the value of,
+ say, $k$ variables and compute the Groebner bases of $q^{k}$ subsystems, where $q$ is the order of the finite
+ field. The Grobner bases computation is done using F5 algorithm.
+
+ .. SEEALSO::
+ :class:`mpkc.algorithms.f5.F5` -- class to compute the complexity of F5 algorithm.
+
+ INPUT:
+
+ - ``problem`` -- MQProblem object including all necessary parameters
+ - ``h`` -- external hybridization parameter (default: 0)
+ - ``w`` -- linear algebra constant (2 <= w <= 3) (default: 2)
+ - ``degrees`` -- a list/tuple of degree of the polynomials (default: [2]*m, i.e. quadratic system)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.hybrid_f5 import HybridF5
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: H = HybridF5(MQProblem(q=256, n=5, m=10))
+ sage: H
+ HybridF5 estimator for the MQ problem with 5 variables and 10 polynomials
+ """
+
+ def __init__(self, problem: MQProblem, **kwargs):
+ q = problem.order_of_the_field()
+ m = problem.npolynomials()
+ if not isinstance(q, (int, Integer)):
+ raise TypeError("q must be an integer")
+
+ degrees = kwargs.get('degrees', [2] * m)
+
+ if len(degrees) != m:
+ raise ValueError(f"len(degrees) must be equal to {m}")
+
+ super().__init__(problem, **kwargs)
+ if degrees == [2] * m:
+ self._degrees = [2] * self.npolynomials_reduced()
+ else:
+ self._degrees = degrees
+ self._name = "HybridF5"
+
+ n = self.nvariables_reduced()
+ self.set_parameter_ranges('k', 0, n - 1)
+
+ def degree_of_polynomials(self):
+ """
+ Return a list of degree of the polynomials
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.hybrid_f5 import HybridF5
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: H = HybridF5(MQProblem(q=31, n=5, m=5), degrees=[3]*5)
+ sage: H.degree_of_polynomials()
+ [3, 3, 3, 3, 3]
+ """
+ return self._degrees
+
+ @optimal_parameter
+ def k(self):
+ """
+ Return the optimal k
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.hybrid_f5 import HybridF5
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: H = HybridF5(MQProblem(q=31, n=23, m=23))
+ sage: H.k()
+ 2
+
+ TESTS::
+
+ sage: H = HybridF5(MQProblem(q=256, n=10, m=10))
+ sage: H.k()
+ 1
+ sage: H = HybridF5(MQProblem(q=256, n=20, m=10))
+ sage: H.k()
+ 1
+ """
+ return self._get_optimal_parameter('k')
+
+ def _compute_time_complexity(self, parameters: dict):
+ """
+ Return the time complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.hybrid_f5 import HybridF5
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: H = HybridF5(MQProblem(q=256, n=10, m=10), bit_complexities=False)
+ sage: H.time_complexity(k=2)
+ 39.98152077132876
+
+ """
+ k = parameters['k']
+ n, m, q = self.get_reduced_parameters()
+ w = self.linear_algebra_constant()
+ degrees = self.degree_of_polynomials()
+ E = F5(MQProblem(n=n-k, m=m, q=q), w=w, degrees=degrees, bit_complexities=False)
+ h = self._h
+ return log2(q) * k + E.time_complexity() + h * log2(q)
+
+ def _compute_memory_complexity(self, parameters: dict):
+ """
+ Return the memory complexity w.r.t. `k`.
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.hybrid_f5 import HybridF5
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: H = HybridF5(MQProblem(q=7, n=10, m=12), bit_complexities=False)
+ sage: H.memory_complexity(k=1)
+ 20.659592676441402
+
+ """
+ k = parameters['k']
+ n, m, q = self.get_reduced_parameters()
+ w = self.linear_algebra_constant()
+ degrees = self.degree_of_polynomials()
+ E = F5(MQProblem(n=n - k, m=m, q=q), w=w, degrees=degrees, bit_complexities=False)
+ return max(E.memory_complexity(), log2(m * n ** 2))
+
+ def _compute_tilde_o_time_complexity(self, parameters: dict):
+ """
+ Return the Ε time complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.hybrid_f5 import HybridF5
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: H = HybridF5(MQProblem(q=7, n=10, m=12), complexity_type=1)
+ sage: H.time_complexity(k=3)
+ 22.23584595738985
+
+ """
+ k = parameters['k']
+ n, m, q = self.get_reduced_parameters()
+ w = self.linear_algebra_constant()
+ degrees = self.degree_of_polynomials()
+ E = F5(MQProblem(n=n - k, m=m, q=q), w=w, degrees=degrees, complexity_type=1)
+ h = self._h
+ return log2(q) * k + E.time_complexity() + h * log2(q)
+
+ def _compute_tilde_o_memory_complexity(self, parameters: dict):
+ """
+ Return the Ε memory complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.hybrid_f5 import HybridF5
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: H = HybridF5(MQProblem(q=7, n=10, m=12), complexity_type=1)
+ sage: H.memory_complexity(k = 3)
+ 12.784634845557521
+
+ """
+ k = parameters['k']
+ n, m, q = self.get_reduced_parameters()
+ w = self.linear_algebra_constant()
+ degrees = self.degree_of_polynomials()
+ E = F5(MQProblem(n=n - k, m=m, q=q), w=w, degrees=degrees, complexity_type=1)
+ return max(E.memory_complexity(), log2(m * n ** 2))
+
+ def _find_optimal_tilde_o_parameters(self):
+ """
+ Return the optimal parameters to achive the optimal Ε time complexity.
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.hybrid_f5 import HybridF5
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = HybridF5(MQProblem(q=7, n=10, m=12), complexity_type=1)
+ sage: E.optimal_parameters()
+ {'k': 3}
+
+ """
+ self._find_optimal_parameters()
diff --git a/cryptographic_estimators/MQEstimator/MQAlgorithms/kpg.py b/cryptographic_estimators/MQEstimator/MQAlgorithms/kpg.py
new file mode 100644
index 00000000..810e5b99
--- /dev/null
+++ b/cryptographic_estimators/MQEstimator/MQAlgorithms/kpg.py
@@ -0,0 +1,118 @@
+# ****************************************************************************
+# 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 ...MQEstimator.mq_algorithm import MQAlgorithm
+from ...MQEstimator.mq_problem import MQProblem
+from ...helper import ComplexityType
+from math import log2
+from sage.all import Integer
+from sage.arith.misc import is_power_of_two
+
+
+class KPG(MQAlgorithm):
+ r"""
+ Construct an instance of KPG estimator
+
+ The KPG is an algorithm to solve a quadratic systems of equations over fields of even characteristic [KPG99]_.
+
+ INPUT:
+
+ - ``problem`` -- MQProblem object including all necessary parameters
+ - ``w`` -- linear algebra constant (2 <= w <= 3) (default: 2)
+ - ``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)
+ - ``complexity_type`` -- complexity type to consider (0: estimate, 1: tilde O comp
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.kpg import KPG
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = KPG(MQProblem(n=183, m=12, q=4), w=2.8)
+ sage: E
+ KPG estimator for the MQ problem with 183 variables and 12 polynomials
+
+ TESTS::
+
+ sage: E.problem.nvariables() == E.nvariables_reduced()
+ True
+ """
+
+ def __init__(self, problem: MQProblem, **kwargs):
+ n, m, q = problem.get_problem_parameters()
+ if not isinstance(q, (int, Integer)):
+ raise TypeError("q must be an integer")
+
+ if not is_power_of_two(q):
+ raise ValueError(
+ "the order of finite field q must be a power of 2")
+
+ if m * (m + 1) >= n:
+ raise ValueError(f'The condition m(m + 1) < n must be satisfied')
+
+ super().__init__(problem, **kwargs)
+ self._name = "KPG"
+ self._n_reduced = n
+ self._m_reduced = m
+
+ def _compute_time_complexity(self, parameters: dict):
+ """
+ Return the time complexity of the algorithm for a given set of parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.kpg import KPG
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = KPG(MQProblem(n=183, m=12, q=4), w=2.8)
+ sage: E.time_complexity()
+ 26.628922047916475
+ """
+ n, m, _ = self.problem.get_problem_parameters()
+ w = self.linear_algebra_constant()
+ return log2(m * n ** w)
+
+ def _compute_memory_complexity(self, parameters: dict):
+ """
+ Return the memory complexity of the algorithm for a given set of parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.kpg import KPG
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = KPG(MQProblem(n=183, m=12, q=4), w=2.8)
+ sage: E.memory_complexity()
+ 19.61636217728924
+ """
+ n, m, _ = self.problem.get_problem_parameters()
+ return log2(m * n ** 2)
+
+ def _compute_tilde_o_time_complexity(self, parameters: dict):
+ """
+ Return the Ε time complexity of the algorithm for a given set of parameters
+
+ """
+ return 0
+
+ def _compute_tilde_o_memory_complexity(self, parameters: dict):
+ """
+ Return the Ε memory complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ """
+ return 0
diff --git a/cryptographic_estimators/MQEstimator/MQAlgorithms/lokshtanov.py b/cryptographic_estimators/MQEstimator/MQAlgorithms/lokshtanov.py
new file mode 100644
index 00000000..8df34a1a
--- /dev/null
+++ b/cryptographic_estimators/MQEstimator/MQAlgorithms/lokshtanov.py
@@ -0,0 +1,240 @@
+# ****************************************************************************
+# 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 ...MQEstimator.series.nmonomial import NMonomialSeries
+from ...MQEstimator.mq_problem import MQProblem
+from ...MQEstimator.mq_algorithm import MQAlgorithm
+from ...base_algorithm import optimal_parameter
+from math import log2
+from sage.functions.other import ceil, floor
+from sage.all import Integer
+from sage.arith.misc import is_power_of_two
+from sage.functions.other import floor
+from sage.rings.infinity import Infinity
+from sage.rings.finite_rings.finite_field_constructor import GF
+
+
+class Lokshtanov(MQAlgorithm):
+ r"""
+ Construct an instance of Lokshtanov et al.'s estimator
+ Lokshtanov et al.'s is a probabilistic algorithm to solve the MQ problem over GF(q) [LPTWY17]_. It describes an
+ algorithm to determine the consistency of a given system of polynomial equations.
+
+ INPUT:
+
+ - ``problem`` --MQProblem object including all necessary parameters
+ - ``h`` -- external hybridization parameter (default: 0)
+ - ``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)
+ - ``complexity_type`` -- complexity type to consider (0: estimate, 1: tilde O complexity, default: 0)
+
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.lokshtanov import Lokshtanov
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Lokshtanov(MQProblem(n=10, m=12, q=9))
+ sage: E
+ Lokshtanov et al. estimator for the MQ problem with 10 variables and 12 polynomials
+ """
+
+ def __init__(self, problem: MQProblem, **kwargs):
+ q = problem.order_of_the_field()
+ if not isinstance(q, (int, Integer)):
+ raise TypeError("q must be an integer")
+
+ if q > 1024:
+ raise TypeError("q too big to run this algorithm")
+
+ super().__init__(problem, **kwargs)
+ self._name = "Lokshtanov et al."
+
+ self.set_parameter_ranges('delta', 0, 1)
+
+ @optimal_parameter
+ def delta(self):
+ r"""
+ Return the optimal `\delta` for Lokshtanov et al.'s algorithm
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.lokshtanov import Lokshtanov
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Lokshtanov(MQProblem(n=10, m=12, q=9))
+ sage: E.delta()
+ 1/10
+ """
+ return self._get_optimal_parameter('delta')
+
+ def _valid_choices(self):
+ """
+ Yields valid values for `delta`.
+ Each call incremetns `l`
+ """
+ n, _, _ = self.get_reduced_parameters()
+ ranges = self._parameter_ranges
+ l_min = max(1, floor(ranges['delta']['min'] * n))
+ l_max = min(ceil(ranges['delta']['max'] * n), n - 1)
+ l = l_min
+ stop = False
+ while not stop:
+ delta_ = l / n
+ yield {'delta': delta_}
+ l += 1
+ if l > l_max:
+ stop = True
+
+ def _C(self, n: int, delta: float):
+ q = self.problem.order_of_the_field()
+ np = floor(delta * n)
+ resulting_degree = 2 * (q - 1) * (np + 2)
+ M = NMonomialSeries(n=n - np, q=q, max_prec=resulting_degree +
+ 1).nmonomials_up_to_degree(resulting_degree)
+ return n * (q ** (n - np) + M * q ** np * n ** (6 * q))
+
+ def _compute_time_complexity(self, parameters: dict):
+ """
+ Return the time complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.lokshtanov import Lokshtanov
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Lokshtanov(MQProblem(n=10, m=12, q=9), bit_complexities=False)
+ sage: E.time_complexity(delta=2/10)
+ 214.16804105519708
+ """
+ delta = parameters['delta']
+ n, _, q = self.get_reduced_parameters()
+ if delta is None:
+ return Infinity
+ else:
+ if not 0 < delta < 1:
+ raise ValueError("delta must be in the range 0 < delta < 1")
+ else:
+ time = 100 * log2(q) * (q - 1) * \
+ sum([self._C(n - i, delta) for i in range(1, n)])
+
+ h = self._h
+ return h * log2(q) + log2(time)
+
+ def _compute_memory_complexity(self, parameters: dict):
+ """
+ Return the memory complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.lokshtanov import Lokshtanov
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Lokshtanov(MQProblem(n=10, m=12, q=9), bit_complexities=False)
+ sage: E.memory_complexity(delta=2/10)
+ 27.471075081419315
+ """
+ delta = parameters['delta']
+ n, _, q = self.get_reduced_parameters()
+
+ if delta is None:
+ return Infinity
+
+ else:
+ np = floor(n * delta)
+ resulting_degree = 2 * (q - 1) * (np + 2)
+ M = NMonomialSeries(n=n - np, q=q, max_prec=resulting_degree +
+ 1).nmonomials_up_to_degree(resulting_degree)
+ memory = M + log2(n) * q ** (n - np)
+
+ return log2(memory)
+
+ def _compute_tilde_o_time_complexity(self, parameters: dict):
+ """
+ Return the Ε time complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.lokshtanov import Lokshtanov
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Lokshtanov(MQProblem(n=10, m=12, q=9), complexity_type=1)
+ sage: E.time_complexity(delta=2/10)
+ 6.339850002884624
+ """
+ delta = parameters['delta']
+ e = 2.718
+ n, _, q = self.get_reduced_parameters()
+ if log2(GF(q).characteristic()) < 8 * e:
+ time = delta * n * log2(q)
+ else:
+ d = GF(q).degree()
+ time = n * log2(q) + (-d * n) * log2((log2(q) / (2 * e * d)))
+
+ h = self._h
+ return h * log2(q) + time
+
+ def _compute_tilde_o_memory_complexity(self, parameters: dict):
+ r"""
+ Return the Ε time complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ TEST::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.lokshtanov import Lokshtanov
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Lokshtanov(MQProblem(n=10, m=12, q=9), complexity_type=1)
+ sage: E.memory_complexity(delta=2/10)
+ 25.359400011538497
+ """
+ delta = parameters['delta']
+ n, _, q = self.get_reduced_parameters()
+ return (1 - delta) * n * log2(q)
+
+ def _find_optimal_tilde_o_parameters(self):
+ """
+ Return the optimal parameters to achieve the optimal Ε time complexity.
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.lokshtanov import Lokshtanov
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = Lokshtanov(MQProblem(n=10, m=12, q=9), complexity_type=1)
+ sage: E.optimal_parameters()
+ {'delta': 0.9975}
+ """
+ n, _, q = self.get_reduced_parameters()
+ e = 2.718
+ if q == 2:
+ delta = 0.8765
+ elif is_power_of_two(q):
+ delta = 0.9
+ elif log2(GF(q).characteristic()) < 8 * e:
+ delta = 0.9975
+ else:
+ delta = None
+ self._optimal_parameters['delta'] = delta
diff --git a/cryptographic_estimators/MQEstimator/MQAlgorithms/mht.py b/cryptographic_estimators/MQEstimator/MQAlgorithms/mht.py
new file mode 100644
index 00000000..e7c25db5
--- /dev/null
+++ b/cryptographic_estimators/MQEstimator/MQAlgorithms/mht.py
@@ -0,0 +1,125 @@
+# ****************************************************************************
+# 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 ...MQEstimator.mq_algorithm import MQAlgorithm
+from ...MQEstimator.mq_problem import MQProblem
+from ...helper import ComplexityType
+from sage.all import Integer
+from sage.arith.misc import is_power_of_two
+from math import log2
+
+
+class MHT(MQAlgorithm):
+ r"""
+ Construct an instance of MHT estimator
+
+ The MHT is an algorithm to solve the MQ problem when $m (m + 3) / 2 \leq n$ [MHT13]_.
+
+ INPUT:
+
+ - ``problem`` -- MQProblem object including all necessary parameters
+ - ``w`` -- linear algebra constant (2 <= w <= 3) (default: 2)
+ - ``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)
+ - ``complexity_type`` -- complexity type to consider (0: estimate, 1: tilde O comp
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.mht import MHT
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = MHT(MQProblem(n=183, m=12, q=4), w=2.8)
+ sage: E
+ MHT estimator for the MQ problem with 183 variables and 12 polynomials
+
+ TESTS::
+
+ sage: E.problem.nvariables() == E.nvariables_reduced()
+ True
+ """
+
+ def __init__(self, problem: MQProblem, **kwargs):
+ n, m, q = problem.get_problem_parameters()
+ if not isinstance(q, (int, Integer)):
+ raise TypeError("q must be an integer")
+
+ if m * (m + 3) / 2 > n:
+ raise ValueError(
+ f'The parameter n should be grater than or equal to m * (m + 3) / 2')
+
+ super().__init__(problem, **kwargs)
+ self._name = "MHT"
+ self._n_reduced = n
+ self._m_reduced = m
+
+ def _compute_time_complexity(self, parameters: dict):
+ """
+ Return the time complexity of the algorithm for a given set of parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.mht import MHT
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = MHT(MQProblem(n=183, m=12, q=4), w=2.8)
+ sage: E.time_complexity()
+ 26.628922047916475
+ """
+ n, m, _ = self.problem.get_problem_parameters()
+ w = self.linear_algebra_constant()
+ if is_power_of_two(self.problem.order_of_the_field()):
+ time = 0
+ else:
+ time = m
+ time += log2(m * n ** w)
+ return time
+
+ def _compute_memory_complexity(self, parameters: dict):
+ """
+ Return the memory complexity of the algorithm for a given set of parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.MQAlgorithms.mht import MHT
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: E = MHT(MQProblem(n=183, m=12, q=4), w=2.8)
+ sage: E.memory_complexity()
+ 19.61636217728924
+ """
+ n, m, q = self.problem.get_problem_parameters()
+ return log2(m * n ** 2)
+
+ def _compute_tilde_o_time_complexity(self, parameters: dict):
+ """
+ Return the Ε time complexity of the algorithm for a given set of parameters
+
+ """
+ _, m, _ = self.get_reduced_parameters()
+ if is_power_of_two(self.problem.order_of_the_field()):
+ time = 0
+ else:
+ time = m
+ return time
+
+ def _compute_tilde_o_memory_complexity(self, parameters: dict):
+ """
+ Return the Ε memory complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ """
+ return 0
diff --git a/cryptographic_estimators/MQEstimator/__init__.py b/cryptographic_estimators/MQEstimator/__init__.py
new file mode 100644
index 00000000..ec9b975a
--- /dev/null
+++ b/cryptographic_estimators/MQEstimator/__init__.py
@@ -0,0 +1,7 @@
+from .degree_of_regularity import generic_system, regular_system, semi_regular_system, quadratic_system
+from .mq_algorithm import MQAlgorithm
+from .mq_estimator import MQEstimator
+from .mq_helper import ngates, nmonomials_of_degree, nmonomials_up_to_degree, sum_of_binomial_coefficients
+from .mq_problem import MQProblem
+from .witness_degree import semi_regular_system, quadratic_system
+from .MQAlgorithms import Bjorklund, BooleanSolveFXL, CGMTA, Crossbred, DinurFirst, DinurSecond, ExhaustiveSearch, F5, HybridF5, KPG, Lokshtanov, MHT
diff --git a/cryptographic_estimators/MQEstimator/degree_of_regularity.py b/cryptographic_estimators/MQEstimator/degree_of_regularity.py
new file mode 100644
index 00000000..2f5be987
--- /dev/null
+++ b/cryptographic_estimators/MQEstimator/degree_of_regularity.py
@@ -0,0 +1,157 @@
+# ****************************************************************************
+# 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 ..MQEstimator.series.hilbert import HilbertSeries
+
+
+def generic_system(n: int, degrees: list[int], q=None):
+ """
+ Return the degree of regularity for the system of polynomial equations
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``degrees`` -- a list of integers representing the degree of the polynomials
+ - ``q`` -- order of the finite field (default: None)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator import degree_of_regularity
+ sage: degree_of_regularity.generic_system(5, [2]*10)
+ 3
+
+ TESTS::
+
+ sage: degree_of_regularity.generic_system(10, [3]*5)
+ Traceback (most recent call last):
+ ...
+ ValueError: degree of regularity is defined for system with n <= m
+ """
+ m = len(degrees)
+
+ if n > m:
+ raise ValueError(
+ "degree of regularity is defined for system with n <= m")
+
+ return semi_regular_system(n=n, degrees=degrees, q=q)
+
+
+def regular_system(n: int, degrees: list[int]):
+ """
+ Return the degree of regularity for regular system
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``degree`` -- a list of integers representing the degree of the polynomials
+
+ .. NOTE::
+
+ The degree of regularity for regular system is defined only for systems with equal numbers of variables
+ and polynomials
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator import degree_of_regularity
+ sage: degree_of_regularity.regular_system(15, [2]*15)
+ 16
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator import degree_of_regularity
+ sage: degree_of_regularity.regular_system(15, [2]*16)
+ Traceback (most recent call last):
+ ...
+ ValueError: the number of variables must be equal to the number of polynomials
+ """
+ m = len(degrees)
+ if n != m:
+ raise ValueError(
+ "the number of variables must be equal to the number of polynomials")
+ return semi_regular_system(n=n, degrees=degrees)
+
+
+def semi_regular_system(n: int, degrees: list[int], q=None):
+ r"""
+ Return the degree of regularity for semi-regular system
+
+ The degree of regularity of a semi-regular system $(f_1, \ldots, f_m)$ of respective degrees $d_1, \ldots, d_m$ is
+ given by the index of the first non-positive coefficient of
+
+ .. MATH::
+
+ \dfrac{\prod_{i=1}^{m} (1 - z^{d_i})}{(1 - z)^{n}}
+
+ If the system is defined over a finite field of order `q`, then it is the index of the first non-positive
+ coefficient of the following sequence
+
+ .. MATH::
+
+ \prod_{i=1}^{m} \dfrac{(1 - z^{d_i})}{(1 - z^{q d_i})} \cdot \Bigg( \dfrac{(1 - z^{q})}{(1 - z)} \Bigg)^{n}
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``degrees`` -- a list of integers representing the degree of the polynomials
+ - ``q`` -- order of the finite field (default: None)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator import degree_of_regularity
+ sage: degree_of_regularity.semi_regular_system(10, [2]*15)
+ 4
+ sage: degree_of_regularity.semi_regular_system(10, [2]*15, q=2)
+ 3
+
+ TESTS::
+
+ sage: degree_of_regularity.semi_regular_system(10, [2]*9)
+ Traceback (most recent call last):
+ ...
+ ValueError: the number of polynomials must be >= than the number of variables
+ """
+ m = len(degrees)
+ if m < n:
+ raise ValueError(
+ "the number of polynomials must be >= than the number of variables")
+
+ s = HilbertSeries(n, degrees, q=q)
+ return s.first_nonpositive_integer()
+
+
+def quadratic_system(n: int, m: int, q=None):
+ """
+ Return the degree of regularity for quadratic system
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``m`` -- no. of polynomials
+ - ``q`` -- order of the finite field (default: None)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator import degree_of_regularity
+ sage: degree_of_regularity.quadratic_system(10, 15)
+ 4
+ sage: degree_of_regularity.quadratic_system(10, 15, q=2)
+ 3
+ sage: degree_of_regularity.quadratic_system(15, 15)
+ 16
+ """
+ return generic_system(n, [2] * m, q=q)
diff --git a/cryptographic_estimators/MQEstimator/mq_algorithm.py b/cryptographic_estimators/MQEstimator/mq_algorithm.py
new file mode 100644
index 00000000..d92f47dc
--- /dev/null
+++ b/cryptographic_estimators/MQEstimator/mq_algorithm.py
@@ -0,0 +1,152 @@
+# ****************************************************************************
+# 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 BaseAlgorithm
+from .mq_problem import MQProblem
+from sage.arith.misc import is_prime_power
+from sage.functions.other import floor
+
+
+class MQAlgorithm(BaseAlgorithm):
+ def __init__(self, problem: MQProblem, **kwargs):
+ """
+ Base class for MQ algorithms complexity estimator
+
+ INPUT:
+
+ - ``problem`` -- BaseProblem object including all necessary parameters
+ - ``w`` -- linear algebra constant (default: 2)
+ - ``h`` -- external hybridization parameter (default: 0)
+ - ``theta`` -- exponent of the conversion factor (default: 2)
+ - ``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)
+ - ``complexity_type`` -- complexity type to consider (0: estimate, 1: tilde O complexity, default: 0)
+
+ """
+ super(MQAlgorithm, self).__init__(problem, **kwargs)
+
+ h = kwargs.get("h", 0)
+ w = kwargs.get("w", 2)
+ theta = kwargs.get("theta", 2)
+ n = self.problem.nvariables()
+ m = self.problem.npolynomials()
+ q = self.problem.order_of_the_field()
+ self._name = "BaseMQAlgorithm"
+
+ if n < 1:
+ raise ValueError("n must be >= 1")
+
+ if m < 1:
+ raise ValueError("m must be >= 1")
+
+ if q is not None and not is_prime_power(q):
+ raise ValueError("q must be a prime power")
+
+ if w is not None and not 2 <= w <= 3:
+ raise ValueError("w must be in the range 2 <= w <= 3")
+
+ if h < 0:
+ raise ValueError("h must be >= 0")
+
+ if theta > 2 or theta < 0:
+ raise ValueError("theta must be in the range 0 <= theta <= 2")
+
+ self._n = n
+ self._m = m
+ self._q = q
+ self._w = w
+ self._h = h
+ self.problem.theta = theta
+ self._n_reduced = None
+ self._m_reduced = None
+
+ def nvariables_reduced(self):
+ """
+ Return the no. of variables after fixing some values
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.mq_algorithm import MQAlgorithm
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: MQAlgorithm(MQProblem(n=5, m=10, q=2)).nvariables_reduced()
+ 5
+ sage: MQAlgorithm(MQProblem(n=25, m=20, q=2)).nvariables_reduced()
+ 20
+ """
+ if self._n_reduced is not None:
+ return self._n_reduced
+
+ n, m = self.problem.nvariables(), self.problem.npolynomials()
+ if self.problem.is_underdefined_system():
+ alpha = floor(n / m)
+ if m - alpha + 1 > 1:
+ self._n_reduced = m - alpha + 1
+ else:
+ self._n_reduced = m
+ else:
+ self._n_reduced = n
+
+ self._n_reduced -= self._h
+
+ if self.problem.is_underdefined_system():
+ self.problem.nsolutions = 0
+ return self._n_reduced
+
+ def npolynomials_reduced(self):
+ """
+ Return the no. of polynomials after applying the Thomae and Wolf strategy
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.mq_algorithm import MQAlgorithm
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: MQAlgorithm(MQProblem(n=5, m=10, q=2)).npolynomials_reduced()
+ 10
+ sage: MQAlgorithm(MQProblem(n=60, m=20, q=2)).npolynomials_reduced()
+ 18
+ """
+ if self._m_reduced is not None:
+ return self._m_reduced
+
+ n, m = self.problem.nvariables(), self.problem.npolynomials()
+ if self.problem.is_underdefined_system():
+ self._m_reduced = self.nvariables_reduced()
+ else:
+ self._m_reduced = m
+ return self._m_reduced
+
+ def get_reduced_parameters(self):
+ return self.nvariables_reduced(), self.npolynomials_reduced(), self.problem.order_of_the_field()
+
+ def linear_algebra_constant(self):
+ """
+ Return the linear algebra constant
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.mq_algorithm import MQAlgorithm
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: MQAlgorithm(MQProblem(n=10, m=5, q=4), w=2).linear_algebra_constant()
+ 2
+ """
+ return self._w
+
+ def __repr__(self):
+ """
+ """
+ n, m = self.problem.nvariables(), self.problem.npolynomials()
+ return f"{self._name} estimator for the MQ problem with {n} variables and {m} polynomials"
diff --git a/cryptographic_estimators/MQEstimator/mq_constants.py b/cryptographic_estimators/MQEstimator/mq_constants.py
new file mode 100644
index 00000000..e0291079
--- /dev/null
+++ b/cryptographic_estimators/MQEstimator/mq_constants.py
@@ -0,0 +1,25 @@
+# ****************************************************************************
+# 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 .
+# ****************************************************************************
+
+
+MQ_NUMBER_VARIABLES = "number of variables"
+MQ_NUMBER_POLYNOMIALS = "number of polynomials"
+MQ_FIELD_SIZE = "field size"
+
+MQ_LAS_VEGAS = "las_vegas"
+MQ_DETERMINISTIC = "deterministic"
+MQ_VARIANT = "variant"
diff --git a/cryptographic_estimators/MQEstimator/mq_estimator.py b/cryptographic_estimators/MQEstimator/mq_estimator.py
new file mode 100644
index 00000000..d7113a31
--- /dev/null
+++ b/cryptographic_estimators/MQEstimator/mq_estimator.py
@@ -0,0 +1,159 @@
+# ****************************************************************************
+# 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 ..MQEstimator.mq_algorithm import MQAlgorithm
+from ..MQEstimator.mq_problem import MQProblem
+from ..base_estimator import BaseEstimator
+from math import inf
+
+
+class MQEstimator(BaseEstimator):
+ """
+ Construct an instance of MQEstimator
+
+ INPUT:
+
+ - ``n`` -- number of variables
+ - ``m`` -- number of polynomials
+ - ``q`` -- order of the finite field (default: None)
+ - ``w`` -- linear algebra constant (default: 2)
+ - ``theta`` -- bit complexity exponent (default: 2)
+ - ``h`` -- external hybridization parameter (default: 0)
+ - ``nsolutions`` -- number of solutions in logarithmic scale (default: max(expected_number_solutions, 0))
+ - ``excluded_algorithms`` -- a list/tuple of excluded algorithms (default: None)
+ - ``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)
+ - ``complexity_type`` -- complexity type to consider (0: estimate, 1: tilde O complexity, default 0)
+ - ``bit_complexities`` -- state complexity as bit rather than field operations (default 1, only relevant for complexity_type 0)
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator import MQEstimator
+ sage: E = MQEstimator(q=2, m=42, n=41, memory_bound=45)
+ sage: E.table() # long time
+ +------------------+---------------+
+ | | estimate |
+ +------------------+------+--------+
+ | algorithm | time | memory |
+ +------------------+------+--------+
+ | Bjorklund | 80.3 | 31.5 |
+ | BooleanSolveFXL | 46.3 | 16.1 |
+ | Crossbred | 39.8 | 37.9 |
+ | DinurFirst | 57.7 | 37.9 |
+ | DinurSecond | 42.8 | 33.6 |
+ | ExhaustiveSearch | 44.4 | 16.1 |
+ | F5 | 62.9 | 57.0 |
+ | HybridF5 | 48.6 | 16.1 |
+ | Lokshtanov | 93.9 | 42.4 |
+ +------------------+------+--------+
+
+ sage: E = MQEstimator(n=15, m=15, q=2)
+ sage: E.table(precision=3, truncate=1)
+ +------------------+-----------------+
+ | | estimate |
+ +------------------+--------+--------+
+ | algorithm | time | memory |
+ +------------------+--------+--------+
+ | Bjorklund | 42.451 | 15.316 |
+ | BooleanSolveFXL | 20.339 | 11.720 |
+ | Crossbred | 17.672 | 16.785 |
+ | DinurFirst | 32.111 | 19.493 |
+ | DinurSecond | 20.349 | 15.801 |
+ | ExhaustiveSearch | 17.966 | 11.720 |
+ | F5 | 27.747 | 23.158 |
+ | HybridF5 | 21.076 | 11.720 |
+ | Lokshtanov | 67.123 | 16.105 |
+ +------------------+--------+--------+
+
+ """
+
+ def __init__(self, n: int, m: int, q=None, memory_bound=inf, **kwargs):
+ super(MQEstimator, self).__init__(MQAlgorithm, MQProblem(
+ n=n, m=m, q=q, memory_bound=memory_bound, **kwargs), **kwargs)
+
+ def table(self, show_quantum_complexity=0, show_tilde_o_time=0, show_all_parameters=0, precision=1, truncate=0):
+ """
+ Print table describing the complexity of each algorithm and its optimal parameters
+
+ INPUT:
+
+ - ``show_quantum_complexity`` -- show quantum time complexity (default: true)
+ - ``show_tilde_o_time`` -- show Ε time complexity (default: true)
+ - ``show_all_parameters`` -- show all optimization parameters (default: true)
+ - ``precision`` -- number of decimal digits output (default: 1)
+ - ``truncate`` -- truncate rather than round the output (default: false)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator import MQEstimator
+ sage: E = MQEstimator(q=3, m=42, n=41, memory_bound=45)
+ sage: E.table() # long time
+ +------------------+----------------+
+ | | estimate |
+ +------------------+-------+--------+
+ | algorithm | time | memory |
+ +------------------+-------+--------+
+ | BooleanSolveFXL | 68.8 | 26.1 |
+ | Crossbred | 60.4 | 44.5 |
+ | ExhaustiveSearch | 67.1 | 17.1 |
+ | F5 | 78.3 | 71.9 |
+ | HybridF5 | 67.8 | 26.7 |
+ | Lokshtanov | 174.5 | 44.9 |
+ +------------------+-------+--------+
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator import MQEstimator
+ sage: E = MQEstimator(q=16, m=42, n=41, complexity_type=1)
+ sage: E.table(show_tilde_o_time=1, show_all_parameters=1) # long time
+ +------------------+-------------------------------------------------------+-------------------------------------------------------+
+ | | estimate | tilde_o_estimate |
+ +------------------+-------+--------+--------------------------------------+-------+--------+--------------------------------------+
+ | algorithm | time | memory | parameters | time | memory | parameters |
+ +------------------+-------+--------+--------------------------------------+-------+--------+--------------------------------------+
+ | BooleanSolveFXL | 107.8 | 71.5 | {'k': 7, 'variant': 'deterministic'} | 98.4 | 70.4 | {'k': 7, 'variant': 'deterministic'} |
+ | Crossbred | 93.6 | 89.7 | {'D': 16, 'd': 7, 'k': 32} | 87.8 | 87.7 | {'D': 16, 'd': 7, 'k': 32} |
+ | ExhaustiveSearch | 167.4 | 18.1 | {} | 164.0 | 0.0 | {} |
+ | F5 | 120.5 | 111.9 | {} | 111.1 | 109.9 | {} |
+ | HybridF5 | 104.6 | 72.4 | {'k': 6} | 95.2 | 70.4 | {'k': 6} |
+ | Lokshtanov | 626.3 | 164.4 | {'delta': 1/41} | 147.6 | 16.4 | {'delta': 0.9} |
+ +------------------+-------+--------+--------------------------------------+-------+--------+--------------------------------------+
+
+
+ sage: E = MQEstimator(q=2, m=42, n=41)
+ sage: E.table(show_tilde_o_time=1, show_all_parameters=1) # long time
+ +------------------+---------------------------------------------------+-------------------------------------------------------------------+
+ | | estimate | tilde_o_estimate |
+ +------------------+------+--------+-----------------------------------+------+--------+---------------------------------------------------+
+ | algorithm | time | memory | parameters | time | memory | parameters |
+ +------------------+------+--------+-----------------------------------+------+--------+---------------------------------------------------+
+ | Bjorklund | 80.3 | 31.5 | {'lambda_': 13/41} | 32.9 | 32.9 | {'lambda_': 0.19677} |
+ | BooleanSolveFXL | 46.3 | 16.1 | {'k': 40, 'variant': 'las_vegas'} | 43.2 | 16.1 | {'k': 40, 'variant': 'las_vegas'} |
+ | Crossbred | 39.8 | 37.9 | {'D': 6, 'd': 1, 'k': 15} | 38.0 | 37.9 | {'D': 6, 'd': 1, 'k': 15} |
+ | DinurFirst | 57.7 | 37.9 | {'kappa': 13/40, 'lambda_': 7/40} | 28.5 | 28.5 | {'kappa': 0.3057, 'lambda_': 0.18665241123894338} |
+ | DinurSecond | 42.8 | 33.6 | {'n1': 7} | 33.4 | 25.8 | {'n1': 7.592592592592592} |
+ | ExhaustiveSearch | 44.4 | 16.1 | {} | 41.0 | 0.0 | {} |
+ | F5 | 62.9 | 57.0 | {} | 57.5 | 57.0 | {} |
+ | HybridF5 | 48.6 | 16.1 | {'k': 40} | 43.2 | 16.1 | {'k': 40} |
+ | Lokshtanov | 93.9 | 42.4 | {'delta': 1/41} | 35.9 | 5.1 | {'delta': 0.8765} |
+ +------------------+------+--------+-----------------------------------+------+--------+---------------------------------------------------+
+ """
+
+ super(MQEstimator, self).table(show_quantum_complexity=show_quantum_complexity,
+ show_tilde_o_time=show_tilde_o_time,
+ show_all_parameters=show_all_parameters,
+ precision=precision, truncate=truncate)
diff --git a/cryptographic_estimators/MQEstimator/mq_helper.py b/cryptographic_estimators/MQEstimator/mq_helper.py
new file mode 100644
index 00000000..7a5b28ca
--- /dev/null
+++ b/cryptographic_estimators/MQEstimator/mq_helper.py
@@ -0,0 +1,121 @@
+# ****************************************************************************
+# 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 ..MQEstimator.series.nmonomial import NMonomialSeries
+from math import log2
+from sage.arith.misc import is_prime_power
+from sage.functions.other import binomial
+
+
+def ngates(q, n, theta=2):
+ """
+ Return the number of gates for the given number of multiplications in a finite field
+
+ INPUT:
+
+ - ``q`` -- order of the finite field
+ - ``n`` -- no. of multiplications (logarithmic)
+ - ``theta`` -- exponent of the conversion factor
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.mq_helper import ngates
+ sage: ngates(16, 16)
+ 20.0
+
+ TESTS::
+
+ sage: ngates(6, 2**16)
+ Traceback (most recent call last):
+ ...
+ ValueError: q must be a prime power
+ """
+ if not is_prime_power(q):
+ raise ValueError("q must be a prime power")
+ if theta:
+ return n + log2(log2(q)) * theta
+ else:
+ return n + log2(2 * log2(q) ** 2 + log2(q))
+
+
+def nmonomials_of_degree(d, n, q):
+ """
+ Return the number of `n`-variables monomials of degree `d`
+
+ .. NOTE::
+
+ If `q` is provided, then it considers the monomials in a ring modulo the ideal generated by the field equations
+
+ INPUT:
+
+ - ``d`` -- degree
+ - ``n`` -- no. of variables
+ - ``q`` -- order of finite field
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.mq_helper import nmonomials_of_degree
+ sage: nmonomials_of_degree(d=2, n=10, q=2)
+ 45
+ """
+ series = NMonomialSeries(n, q, max_prec=d+1)
+ return series.nmonomials_of_degree(d)
+
+
+def nmonomials_up_to_degree(d, n, q):
+ """
+ Return the number of `n`-variables monomials up to degree `d`
+
+ .. NOTE::
+
+ If `q` is provided, then it considers the monomials in a ring modulo the ideal generated by the field equations
+
+ INPUT:
+
+ - ``d`` -- degree
+ - ``n`` -- no. of variables
+ - ``q`` -- order of finite field
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.mq_helper import nmonomials_up_to_degree
+ sage: nmonomials_up_to_degree(d=2, n=10, q=2)
+ 56
+ """
+ series = NMonomialSeries(n, q, max_prec=d+1)
+ return series.nmonomials_up_to_degree(d)
+
+
+def sum_of_binomial_coefficients(n, l):
+ r"""
+ Return the `\sum_{j=0}^{l} \binom{n}{j}`
+
+ INPUT:
+
+ - ``n`` -- a non-negative integer
+ - ``l`` -- a non-negative integer
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.mq_helper import sum_of_binomial_coefficients
+ sage: sum_of_binomial_coefficients(5, 2)
+ 16
+ """
+ if l < 0:
+ raise ValueError('l must be a non-negative integer')
+ return sum(binomial(n, j) for j in range(l + 1))
diff --git a/cryptographic_estimators/MQEstimator/mq_problem.py b/cryptographic_estimators/MQEstimator/mq_problem.py
new file mode 100644
index 00000000..edc176af
--- /dev/null
+++ b/cryptographic_estimators/MQEstimator/mq_problem.py
@@ -0,0 +1,210 @@
+# ****************************************************************************
+# 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_problem import BaseProblem
+from ..MQEstimator.mq_helper import ngates
+from math import log2
+from sage.functions.other import ceil
+from .mq_constants import *
+from sage.arith.misc import is_prime_power
+
+class MQProblem(BaseProblem):
+ """
+ Construct an instance of MQProblem
+
+ INPUT:
+
+ - ``n`` -- number of variables
+ - ``m`` -- number of polynomials
+ - ``q`` -- order of the finite field (default: None)
+ - ``nsolutions`` -- number of solutions in logarithmic scale (default: max(expected_number_solutions, 0))
+ - ``memory_bound`` -- maximum allowed memory to use for solving the problem
+
+ """
+
+ def __init__(self, n: int, m: int, q:int, **kwargs):
+ if n < 1:
+ raise ValueError("n must be >= 1")
+
+ if m < 1:
+ raise ValueError("m must be >= 1")
+
+ if q is not None and not is_prime_power(q):
+ raise ValueError("q must be a prime power")
+
+ super().__init__(**kwargs)
+ self.parameters[MQ_NUMBER_VARIABLES] = n
+ self.parameters[MQ_NUMBER_POLYNOMIALS] = m
+ self.parameters[MQ_FIELD_SIZE] = q
+ self.nsolutions = kwargs.get("nsolutions", self.expected_number_solutions())
+ self._theta = kwargs.get("theta", 2)
+
+ def to_bitcomplexity_time(self, basic_operations: float):
+ """
+ Returns the bit-complexity corresponding to basic_operations field multiplications
+
+ INPUT:
+
+ - ``basic_operations`` -- Number of field additions (logarithmic)
+
+ """
+ q = self.parameters[MQ_FIELD_SIZE]
+ theta = self._theta
+ return ngates(q, basic_operations, theta=theta)
+
+ @property
+ def theta(self):
+ """
+ returns the runtime of the algorithm
+
+ """
+ return self._theta
+
+ @theta.setter
+ def theta(self, value: float):
+ """
+ sets the runtime
+
+ """
+ self._theta = value
+
+ def to_bitcomplexity_memory(self, elements_to_store: float):
+ """
+ Returns the memory bit-complexity associated to a given number of elements to store
+
+ INPUT:
+
+ -``elements_to_store`` -- number of basic memory operations (logarithmic)
+
+ """
+ q = self.parameters[MQ_FIELD_SIZE]
+ if q is None:
+ return elements_to_store
+ return log2(ceil(log2(q))) + elements_to_store
+
+ def expected_number_solutions(self):
+ """
+ Returns the logarithm of the expected number of existing solutions to the problem
+ """
+ n, m, q = self.get_problem_parameters()
+ return max(0, log2(q) * (n - m))
+
+ def order_of_the_field(self):
+ """
+ Return the order of the field
+
+ """
+ q = self.parameters[MQ_FIELD_SIZE]
+ return q
+
+ def is_defined_over_finite_field(self):
+ """
+ Return `True` if the algorithm is defined over a finite field
+
+ """
+ return self.order_of_the_field()
+
+ def npolynomials(self):
+ """"
+ Return the number of polynomials
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: MQProblem(n=10, m=5, q=4).npolynomials()
+ 5
+ """
+ return self.parameters[MQ_NUMBER_POLYNOMIALS]
+
+ def nvariables(self):
+ """
+ Return the number of variables
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: MQProblem(n=10, m=5, q=4).nvariables()
+ 10
+ """
+ return self.parameters[MQ_NUMBER_VARIABLES]
+
+ def get_problem_parameters(self):
+ """
+ Returns n, m, q
+ """
+ return self.parameters[MQ_NUMBER_VARIABLES], self.parameters[MQ_NUMBER_POLYNOMIALS], self.parameters[MQ_FIELD_SIZE]
+
+ def is_overdefined_system(self):
+ """
+ Return `True` if the system is overdefined
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: MQProblem(n=10, m=15, q=4).is_overdefined_system()
+ True
+ sage: MQProblem(n=10, m=5, q=4).is_overdefined_system()
+ False
+ sage: MQProblem(n=10, m=5, q=4).is_overdefined_system()
+ False
+ """
+ return self.npolynomials() > self.nvariables()
+
+ def is_underdefined_system(self):
+ """
+ Return `True` if the system is underdefined
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: MQProblem(n=10, m=5, q=4).is_underdefined_system()
+ True
+ sage: MQProblem(n=5, m=10, q=4).is_underdefined_system()
+ False
+ sage: MQProblem(n=10, m=10, q=4).is_underdefined_system()
+ False
+ """
+ return self.nvariables() > self.npolynomials()
+
+ def is_square_system(self):
+ """
+ Return `True` is the system is square, i.e. there are equal no. of variables and polynomials
+
+ TESTS::
+
+ sage: from cryptographic_estimators.MQEstimator.mq_problem import MQProblem
+ sage: MQProblem(n=10, m=10, q=4).is_square_system()
+ True
+ sage: MQProblem(n=10, m=5, q=4).is_square_system()
+ False
+ """
+ return self.nvariables() == self.npolynomials()
+
+ def __repr__(self):
+ """
+ """
+ n, m, q = self.get_problem_parameters()
+ rep = "MQ problem with (n,m,q) = " \
+ + "(" + str(n) + "," + str(m) + "," + \
+ str(q) + ") over " + str(self.baseField)
+
+ return rep
diff --git a/cryptographic_estimators/MQEstimator/series/__init__.py b/cryptographic_estimators/MQEstimator/series/__init__.py
new file mode 100644
index 00000000..d7d0c6b2
--- /dev/null
+++ b/cryptographic_estimators/MQEstimator/series/__init__.py
@@ -0,0 +1,2 @@
+from .hilbert import HilbertSeries
+from .nmonomial import NMonomialSeries
\ No newline at end of file
diff --git a/cryptographic_estimators/MQEstimator/series/hilbert.py b/cryptographic_estimators/MQEstimator/series/hilbert.py
new file mode 100644
index 00000000..8f70eb08
--- /dev/null
+++ b/cryptographic_estimators/MQEstimator/series/hilbert.py
@@ -0,0 +1,177 @@
+# ****************************************************************************
+# 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 sage.all import ZZ, QQ
+from sage.misc.misc_c import prod
+from sage.rings.power_series_ring import PowerSeriesRing
+from sage.arith.misc import is_prime_power
+
+
+class HilbertSeries(object):
+ """
+ Construct an instance of Hilbert series
+
+ INPUT:
+
+ - ``n`` -- no of variables
+ - ``degrees`` -- a list of integers representing the degree of the polynomials
+ - ``q`` -- order of the finite field (default: None)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.series.hilbert import HilbertSeries
+ sage: H = HilbertSeries(10, [2]*15)
+ sage: H
+ Hilbert series for system with 10 variables and 15 polynomials
+ sage: H = HilbertSeries(10, [2]*15, q=2)
+ sage: H
+ Hilbert series for system with 10 variables and 15 polynomials over F_2
+ """
+
+ def __init__(self, n: int, degrees: list[int], q=None):
+ self._q = q
+ self._nvariables = n
+ self._degrees = degrees
+ self._ring = PowerSeriesRing(
+ QQ, 'z', default_prec=2*len(self._degrees))
+ z = self._ring.gen()
+ if q is not None:
+ if not is_prime_power(q):
+ raise ValueError(
+ "the order of finite field q must be a prime power")
+
+ if q < 2 * len(self._degrees):
+ self._series = prod([(1 - z ** d) / (1 - z ** (d * q))
+ for d in degrees]) * ((1 - z ** q) / (1 - z)) ** n
+ else:
+ self._series = prod([1 - z ** d for d in degrees]) / (1 - z) ** n
+ else:
+ self._series = prod([1 - z ** d for d in degrees]) / (1 - z) ** n
+
+ @property
+ def nvariables(self):
+ """
+ Return the no. of variables
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.series.hilbert import HilbertSeries
+ sage: H = HilbertSeries(5, [2]*7)
+ sage: H.nvariables
+ 5
+ """
+ return self._nvariables
+
+ @property
+ def degrees(self):
+ """
+ Return a list of degrees of the polynomials
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.series.hilbert import HilbertSeries
+ sage: H = HilbertSeries(5, [2]*7)
+ sage: H.degrees
+ [2, 2, 2, 2, 2, 2, 2]
+ """
+ return self._degrees
+
+ @property
+ def precision(self):
+ """
+ Return the default precision of the series
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.series.hilbert import HilbertSeries
+ sage: H = HilbertSeries(5, [2]*7)
+ sage: H.precision
+ 14
+ """
+ return self.ring.default_prec()
+
+ @property
+ def ring(self):
+ """
+ Return the power series ring
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.series.hilbert import HilbertSeries
+ sage: H = HilbertSeries(5, [2]*7)
+ sage: H.ring
+ Power Series Ring in z over Rational Field
+ """
+ return self._ring
+
+ @property
+ def series(self):
+ """
+ Return the series
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.series.hilbert import HilbertSeries
+ sage: H = HilbertSeries(4, [2]*5)
+ sage: H.series
+ 1 + 4*z + 5*z^2 - 5*z^4 - 4*z^5 - z^6 + O(z^10)
+ sage: H = HilbertSeries(4, [2]*5, q=2)
+ sage: H.series
+ 1 + 4*z + z^2 - 16*z^3 - 14*z^4 + 40*z^5 + 50*z^6 - 80*z^7 - 125*z^8 + 140*z^9 + O(z^10)
+ """
+ return self._series
+
+ @property
+ def npolynomials(self):
+ """
+ Return the no. of polynomials
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.series.hilbert import HilbertSeries
+ sage: H = HilbertSeries(10, [2]*15)
+ sage: H.npolynomials
+ 15
+ """
+ return len(self._degrees)
+
+ def first_nonpositive_integer(self):
+ """
+ Return the first non-positive integer of the series
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.series.hilbert import HilbertSeries
+ sage: H = HilbertSeries(10, [2]*15)
+ sage: H.first_nonpositive_integer()
+ 4
+ """
+ s = self.series()
+ for d in range(self.precision):
+ if s[d] <= 0:
+ return ZZ(d)
+ raise ValueError(
+ "unable to find a nonpositive coefficient in the series")
+
+ def __repr__(self):
+ """
+ """
+ text = f"Hilbert series for system with {self.nvariables} variables and {self.npolynomials} polynomials"
+ if self._q is not None:
+ text += f" over F_{self._q}"
+ return text
diff --git a/cryptographic_estimators/MQEstimator/series/nmonomial.py b/cryptographic_estimators/MQEstimator/series/nmonomial.py
new file mode 100644
index 00000000..c656dfb1
--- /dev/null
+++ b/cryptographic_estimators/MQEstimator/series/nmonomial.py
@@ -0,0 +1,145 @@
+# ****************************************************************************
+# 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 sage.arith.misc import is_prime_power
+from sage.rings.power_series_ring import PowerSeriesRing
+from sage.rings.all import QQ
+
+
+class NMonomialSeries(object):
+ """
+ Construct an instance of the series of a polynomial ring
+
+ INPUT:
+
+ - ``n`` -- the number of variables
+ - ``q`` -- the size of the field (default: None)
+ - ``max_prec`` -- degree of the series (default: None)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.series.nmonomial import NMonomialSeries
+ sage: NM = NMonomialSeries(n=6, q=5)
+ sage: NM
+ Class for the number of monomials in the polynomial ring in 6 variables over F_5
+ """
+
+ def __init__(self, n: int, q=None, max_prec=None):
+ self._n = n
+ if max_prec is not None:
+ self._max_prec = max_prec
+ else:
+ self._max_prec = self._n + 1
+
+ R = PowerSeriesRing(QQ, 'z', default_prec=self._max_prec)
+ z = R.gen()
+
+ if q is not None:
+ if not is_prime_power(q):
+ raise ValueError(
+ "the order of finite field q must be a prime power")
+ self._q = q
+ if q < self._max_prec:
+ self._series_of_degree = (
+ (1 - z ** self._q) ** self._n) / ((1 - z) ** self._n)
+ else:
+ self._series_of_degree = 1 / ((1 - z) ** self._n)
+ else:
+ self._series_of_degree = 1 / ((1 - z) ** self._n)
+
+ self._series_up_to_degree = self._series_of_degree / (1 - z)
+
+ def series_monomials_of_degree(self):
+ """
+ Return the series of the number of monomials of a given degree
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.series.nmonomial import NMonomialSeries
+ sage: NM = NMonomialSeries(n=6, q=5)
+ sage: NM.series_monomials_of_degree()
+ 1 + 6*z + 21*z^2 + 56*z^3 + 126*z^4 + 246*z^5 + 426*z^6 + O(z^7)
+ """
+ return self._series_of_degree
+
+ def series_monomials_up_to_degree(self):
+ """
+ Return the series of the number of monomials up to given degree
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.series.nmonomial import NMonomialSeries
+ sage: NM = NMonomialSeries(n=6, q=5)
+ sage: NM.series_monomials_up_to_degree()
+ 1 + 7*z + 28*z^2 + 84*z^3 + 210*z^4 + 456*z^5 + 882*z^6 + O(z^7)
+ """
+ return self._series_up_to_degree
+
+ def nmonomials_of_degree(self, d: int):
+ """
+ Return the number of monomials of degree `d`
+
+ INPUT:
+
+ - ``d`` -- a non-negative integer
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.series.nmonomial import NMonomialSeries
+ sage: NM = NMonomialSeries(n=6, q=5)
+ sage: NM.nmonomials_of_degree(4)
+ 126
+ """
+ max_prec = self._max_prec
+ if d < max_prec:
+ return self._series_of_degree[d]
+
+ raise ValueError(
+ f'The degree d should be smaller than the precision of the series which is {self._max_prec}')
+
+ def nmonomials_up_to_degree(self, d: int):
+ """
+ Return the number of monomials up to degree `d`
+
+ INPUT:
+
+ - ``d`` -- a non-negative integer
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator.series.nmonomial import NMonomialSeries
+ sage: NM = NMonomialSeries(n=6, q=5)
+ sage: NM.nmonomials_up_to_degree(4)
+ 210
+ """
+ max_prec = self._max_prec
+ if d < max_prec:
+ return self._series_up_to_degree[d]
+
+ raise ValueError(
+ f'The degree d should be smaller than the precision of the series which is {max_prec}')
+
+ def __repr__(self):
+ """
+ """
+ n = self._n
+ q = self._q
+ if q is None:
+ return f'Class for the number of monomials in the polynomial ring in {n} variables'
+ else:
+ return f'Class for the number of monomials in the polynomial ring in {n} variables over F_{q}'
diff --git a/cryptographic_estimators/MQEstimator/witness_degree.py b/cryptographic_estimators/MQEstimator/witness_degree.py
new file mode 100644
index 00000000..f4a33fbf
--- /dev/null
+++ b/cryptographic_estimators/MQEstimator/witness_degree.py
@@ -0,0 +1,74 @@
+# ****************************************************************************
+# 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 ..MQEstimator.series.hilbert import HilbertSeries
+
+
+def semi_regular_system(n: int, degrees: list[int], q=None):
+ """
+ Return the witness degree for semi-regular system
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``degrees`` -- a list of integers representing the degree of the polynomials
+ - ``q`` -- order of the finite field (default: None)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator import witness_degree
+ sage: witness_degree.semi_regular_system(10, [2]*15)
+ 5
+ sage: witness_degree.semi_regular_system(10, [2]*15, q=2)
+ 4
+ """
+ m = len(degrees)
+ if m <= n and q is None:
+ raise ValueError(
+ "The number of polynomials must be greater than the number of variables")
+ elif m < n and q is not None:
+ raise ValueError(
+ "The number of polynomials must be greater than or equal to the number of variables")
+
+ s = HilbertSeries(n, degrees, q=q)
+ z = s.ring.gen()
+ s._series /= (1 - z)
+ return s.first_nonpositive_integer()
+
+
+def quadratic_system(n: int, m: int, q=None):
+ """
+ Return the witness degree for quadratic system
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``m`` -- no. of polynomials
+ - ``q`` -- order of the finite field (default: None)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.MQEstimator import witness_degree
+ sage: witness_degree.quadratic_system(10, 15)
+ 5
+ sage: witness_degree.quadratic_system(10, 15, q=2)
+ 4
+ sage: witness_degree.quadratic_system(15, 15, q=7)
+ 12
+ """
+ return semi_regular_system(n, [2] * m, q=q)
diff --git a/cryptographic_estimators/PEEstimator/PEAlgorithms/__init__.py b/cryptographic_estimators/PEEstimator/PEAlgorithms/__init__.py
new file mode 100644
index 00000000..d8c8f514
--- /dev/null
+++ b/cryptographic_estimators/PEEstimator/PEAlgorithms/__init__.py
@@ -0,0 +1,4 @@
+from .leon import Leon
+from .beullens import Beullens
+from .ssa import SSA
+# TODO: Remember to add the algorithms to the import above
\ No newline at end of file
diff --git a/cryptographic_estimators/PEEstimator/PEAlgorithms/beullens.py b/cryptographic_estimators/PEEstimator/PEAlgorithms/beullens.py
new file mode 100644
index 00000000..4f0d1846
--- /dev/null
+++ b/cryptographic_estimators/PEEstimator/PEAlgorithms/beullens.py
@@ -0,0 +1,113 @@
+from ..pe_algorithm import PEAlgorithm
+from ..pe_problem import PEProblem
+from ..pe_constants import *
+from ...base_algorithm import optimal_parameter
+from ..pe_helper import median_size_of_random_orbit, hamming_ball
+from math import log, ceil, log2, inf
+from ...base_constants import BASE_MEMORY_BOUND, BASE_NSOLUTIONS, BASE_BIT_COMPLEXITIES
+from ...SDFqEstimator.sdfq_estimator import SDFqEstimator
+
+
+class Beullens(PEAlgorithm):
+
+ def __init__(self, problem: PEProblem, **kwargs):
+ """
+ Complexity estimate of Beullens algorithm
+
+ Estimates are adapted versions of the scripts derived in [Beu20]_ with the code accessible at
+ https://github.com/WardBeullens/LESS_Attack
+
+ INPUT:
+
+ - ``problem`` -- PEProblem object including all necessary parameters
+ - ``sd_parameters`` -- dictionary of parameters for SDFqEstimator used as a subroutine (default: {})
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.PEEstimator.PEAlgorithms import Beullens
+ sage: from cryptographic_estimators.PEEstimator import PEProblem
+ sage: Beullens(PEProblem(n=100,k=50,q=3))
+ Beullens estimator for permutation equivalence problem with (n,k,q) = (100,50,3)
+
+ """
+ super().__init__(problem, **kwargs)
+ self._name = "Beullens"
+ n, k, _, _ = self.problem.get_parameters()
+ self.set_parameter_ranges('w', 0, n - k)
+
+ self.SDFqEstimator = None
+
+ self._SDFqEstimator_parameters = kwargs.get(PE_SD_PARAMETERS, {})
+ self._SDFqEstimator_parameters.pop(BASE_BIT_COMPLEXITIES, None)
+ self._SDFqEstimator_parameters.pop(BASE_NSOLUTIONS, None)
+ self._SDFqEstimator_parameters.pop(BASE_MEMORY_BOUND, None)
+
+ @optimal_parameter
+ def w(self):
+ """
+ Return the optimal parameter $w$ used in the algorithm optimization
+
+ EXAMPLES::
+ sage: from cryptographic_estimators.PEEstimator.PEAlgorithms import Beullens
+ sage: from cryptographic_estimators.PEEstimator import PEProblem
+ sage: A = Beullens(PEProblem(n=100,k=50,q=31))
+ sage: A.w()
+ 42
+
+ """
+ return self._get_optimal_parameter("w")
+
+ def _time_and_memory_complexity(self, parameters: dict, verbose_information=None):
+ """
+
+ INPUT:
+ - ``parameters`` -- dictionary including parameters
+ - ``verbose_information`` -- if set to a dictionary `lists`,
+ `list_cost` and `norm_factor` will be returned.
+
+ """
+ n, k, q, _ = self.problem.get_parameters()
+ w = parameters["w"]
+
+ search_space_size = hamming_ball(n, q, w) - log2(q) * (n - k) - log2(q - 1)
+ if search_space_size < 1:
+ return inf, inf
+
+ size_of_orbit = median_size_of_random_orbit(n, w, q)
+ if size_of_orbit > log2(q) * (n - k) - log2(ceil(4 * log(n, 2))):
+ return inf, inf
+
+ list_size = (search_space_size + log2(2 * log2(n))) / 2
+
+ self.SDFqEstimator = SDFqEstimator(n=n, k=k, w=w, q=q, bit_complexities=0, nsolutions=0,
+ memory_bound=self.problem.memory_bound, **self._SDFqEstimator_parameters)
+ c_isd = self.SDFqEstimator.fastest_algorithm().time_complexity()
+ m_isd = self.SDFqEstimator.fastest_algorithm().memory_complexity()
+ list_computation = c_isd - search_space_size + list_size + 1
+
+ normal_form_cost = 1 + list_size
+
+ if verbose_information is not None:
+ verbose_information[VerboseInformation.LISTS_SIZE.value] = list_size
+ verbose_information[VerboseInformation.LIST_COMPUTATION.value] = list_computation
+ verbose_information[VerboseInformation.NORMAL_FORM.value] = normal_form_cost
+
+ return max(list_computation, normal_form_cost + log2(n)), max(m_isd, list_size + log2(n))
+
+ def _compute_time_complexity(self, parameters: dict):
+ return self._time_and_memory_complexity(parameters)[0]
+
+ def _compute_memory_complexity(self, parameters: dict):
+ return self._time_and_memory_complexity(parameters)[1]
+
+ def _get_verbose_information(self):
+ """
+ returns a dictionary containing additional algorithm information
+ """
+ verb = dict()
+ _ = self._time_and_memory_complexity(self.optimal_parameters(), verbose_information=verb)
+ return verb
+
+ def __repr__(self):
+ rep = "Beullens estimator for " + str(self.problem)
+ return rep
diff --git a/cryptographic_estimators/PEEstimator/PEAlgorithms/leon.py b/cryptographic_estimators/PEEstimator/PEAlgorithms/leon.py
new file mode 100644
index 00000000..fdfd78ee
--- /dev/null
+++ b/cryptographic_estimators/PEEstimator/PEAlgorithms/leon.py
@@ -0,0 +1,84 @@
+from ...PEEstimator.pe_algorithm import PEAlgorithm
+from ...PEEstimator.pe_problem import PEProblem
+from ...base_algorithm import optimal_parameter
+from ..pe_helper import gv_distance, number_of_weight_d_codewords
+from ...SDFqEstimator.sdfq_estimator import SDFqEstimator
+from math import log, ceil, log2
+
+
+class Leon(PEAlgorithm):
+
+ def __init__(self, problem: PEProblem, **kwargs):
+ """
+ Complexity estimate of Leons algorithm [Leo82]_
+ Estimates are adapted versions of the scripts derived in [Beu20]_ with the code accessible at
+ https://github.com/WardBeullens/LESS_Attack
+
+
+ INPUT:
+
+ - ``problem`` -- PEProblem object including all necessary parameters
+ - ``codewords_needed_for_success`` -- Number of low word codewords needed for success (default = 100)
+ - ``sd_parameters`` -- dictionary of parameters for SDFqEstimator used as a subroutine (default: {})
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.PEEstimator.PEAlgorithms import Leon
+ sage: from cryptographic_estimators.PEEstimator import PEProblem
+ sage: Leon(PEProblem(n=100,k=50,q=3))
+ Leon estimator for permutation equivalence problem with (n,k,q) = (100,50,3)
+
+ """
+ super().__init__(problem, **kwargs)
+ self._name = "Leon"
+ n, k, q, _ = self.problem.get_parameters()
+ self._codewords_needed_for_success = kwargs.get("codewords_needed_for_success",
+ min(100, int(number_of_weight_d_codewords(n, k, q,
+ gv_distance(n, k,
+ q) + 3))))
+ self.set_parameter_ranges('w', 0, n)
+
+ self.SDFqEstimator = None
+
+ self._SDFqEstimator_parameters = kwargs.get("sd_parameters", {})
+ self._SDFqEstimator_parameters.pop("bit_complexities", None)
+ self._SDFqEstimator_parameters.pop("nsolutions", None)
+ self._SDFqEstimator_parameters.pop("memory_bound", None)
+
+ @optimal_parameter
+ def w(self):
+ """
+ Return the optimal parameter $w$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.PEEstimator.PEAlgorithms import Leon
+ sage: from cryptographic_estimators.PEEstimator import PEProblem
+ sage: A = Leon(PEProblem(n=100,k=50,q=3))
+ sage: A.w()
+ 20
+
+ """
+ n, k, q, _ = self.problem.get_parameters()
+ d = gv_distance(n, k, q)
+
+ while number_of_weight_d_codewords(n, k, q, d) < self._codewords_needed_for_success and d < n - k:
+ d += 1
+ return d
+
+ def _compute_time_complexity(self, parameters: dict):
+ n, k, q, _ = self.problem.get_parameters()
+ w = parameters["w"]
+ N = number_of_weight_d_codewords(n, k, q, w)
+ self.SDFqEstimator = SDFqEstimator(n=n, k=k, w=w, q=q, nsolutions=0, memory_bound=self.problem.memory_bound,
+ bit_complexities=0, **self._SDFqEstimator_parameters)
+ c_isd = self.SDFqEstimator.fastest_algorithm().time_complexity()
+ return c_isd + log2(ceil(2 * (0.57 + log(N))))
+
+ def _compute_memory_complexity(self, parameters: dict):
+ n, k, q, _ = self.problem.get_parameters()
+ return self.SDFqEstimator.fastest_algorithm().memory_complexity()
+
+ def __repr__(self):
+ rep = "Leon estimator for " + str(self.problem)
+ return rep
diff --git a/cryptographic_estimators/PEEstimator/PEAlgorithms/ssa.py b/cryptographic_estimators/PEEstimator/PEAlgorithms/ssa.py
new file mode 100644
index 00000000..9f721a5b
--- /dev/null
+++ b/cryptographic_estimators/PEEstimator/PEAlgorithms/ssa.py
@@ -0,0 +1,39 @@
+from ...PEEstimator.pe_algorithm import PEAlgorithm
+from ...PEEstimator.pe_problem import PEProblem
+from math import log, log2
+
+
+class SSA(PEAlgorithm):
+
+ def __init__(self, problem: PEProblem, **kwargs):
+ """
+ Complexity estimate of Support Splitting Algorithm [Sen06]_
+ Rough Estimate according to [BBPS20]_
+
+ INPUT:
+
+ - ``problem`` -- PEProblem object including all necessary parameters
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.PEEstimator.PEAlgorithms import SSA
+ sage: from cryptographic_estimators.PEEstimator import PEProblem
+ sage: SSA(PEProblem(n=100,k=50,q=3))
+ Support Splitting Algorithm estimator for permutation equivalence problem with (n,k,q) = (100,50,3)
+
+ """
+
+ super().__init__(problem, **kwargs)
+ self._name = "SSA"
+
+ def _compute_time_complexity(self, parameters: dict):
+ n, _, q, h = self.problem.get_parameters()
+ return log2(n ** 3 + n ** 2 * q ** h * log(h))
+
+ def _compute_memory_complexity(self, parameters: dict):
+ n, k, q, h = self.problem.get_parameters()
+ return log2(n * h + n * k + n * (n - k))
+
+ def __repr__(self):
+ rep = "Support Splitting Algorithm estimator for " + str(self.problem)
+ return rep
diff --git a/cryptographic_estimators/PEEstimator/__init__.py b/cryptographic_estimators/PEEstimator/__init__.py
new file mode 100644
index 00000000..a45ecba9
--- /dev/null
+++ b/cryptographic_estimators/PEEstimator/__init__.py
@@ -0,0 +1,7 @@
+from .pe_algorithm import PEAlgorithm
+from .pe_estimator import PEEstimator
+from .pe_problem import PEProblem
+from .PEAlgorithms import Leon
+from .PEAlgorithms import Beullens
+from .PEAlgorithms import SSA
+# TODO: Remember to add the algorithms to the import above
\ No newline at end of file
diff --git a/cryptographic_estimators/PEEstimator/pe_algorithm.py b/cryptographic_estimators/PEEstimator/pe_algorithm.py
new file mode 100644
index 00000000..6e113f29
--- /dev/null
+++ b/cryptographic_estimators/PEEstimator/pe_algorithm.py
@@ -0,0 +1,38 @@
+# ****************************************************************************
+# 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 BaseAlgorithm
+from .pe_problem import PEProblem
+
+
+class PEAlgorithm(BaseAlgorithm):
+ def __init__(self, problem: PEProblem, **kwargs):
+ """
+ Base class for PE algorithms complexity estimator
+
+ INPUT:
+
+ - ``problem`` -- PEProblem object including all necessary parameters
+
+ """
+ super(PEAlgorithm, self).__init__(problem, **kwargs)
+
+ def __repr__(self):
+ """
+ """
+ pass
diff --git a/cryptographic_estimators/PEEstimator/pe_constants.py b/cryptographic_estimators/PEEstimator/pe_constants.py
new file mode 100644
index 00000000..01ebef1c
--- /dev/null
+++ b/cryptographic_estimators/PEEstimator/pe_constants.py
@@ -0,0 +1,15 @@
+from enum import Enum
+
+PE_CODE_LENGTH = "code length"
+PE_CODE_DIMENSION = "code dimension"
+PE_FIELD_SIZE = "field size"
+PE_HULL_DIMENSION = "hull dimension"
+PE_SD_PARAMETERS = "sd_parameters"
+
+
+class VerboseInformation(Enum):
+ """
+ """
+ LIST_COMPUTATION = "list_computation"
+ LISTS_SIZE = "list_size"
+ NORMAL_FORM = "norm_form"
diff --git a/cryptographic_estimators/PEEstimator/pe_estimator.py b/cryptographic_estimators/PEEstimator/pe_estimator.py
new file mode 100644
index 00000000..245922e7
--- /dev/null
+++ b/cryptographic_estimators/PEEstimator/pe_estimator.py
@@ -0,0 +1,97 @@
+# ****************************************************************************
+# 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 ..PEEstimator.pe_algorithm import PEAlgorithm
+from ..PEEstimator.pe_problem import PEProblem
+from ..base_estimator import BaseEstimator
+from math import inf
+
+
+class PEEstimator(BaseEstimator):
+ """
+ Construct an instance of Permutation Code Equivalence Estimator
+
+ INPUT:
+
+ - ``n`` -- code length
+ - ``k`` -- code dimension
+ - ``q`` -- field size
+ - ``excluded_algorithms`` -- a list/tuple of excluded algorithms (default: None)
+ - ``sd_parameters`` -- dictionary of parameters for SDEstimator used as a subroutine by some algorithms (default: {})
+ - ``nsolutions`` -- no. of solutions
+
+ """
+ excluded_algorithms_by_default = []
+
+ def __init__(self, n: int, k: int, q: int, memory_bound=inf, **kwargs): # Add estimator parameters
+ if not kwargs.get("excluded_algorithms"):
+ kwargs["excluded_algorithms"] = []
+
+ kwargs["excluded_algorithms"] += self.excluded_algorithms_by_default
+ super(PEEstimator, self).__init__(
+ PEAlgorithm, PEProblem(n, k, q, memory_bound=memory_bound, **kwargs), **kwargs)
+
+
+ def table(self, show_quantum_complexity=0, show_tilde_o_time=0,
+ show_all_parameters=0, precision=1, truncate=0):
+ """
+ Print table describing the complexity of each algorithm and its optimal parameters
+
+ INPUT:
+
+ - ``show_quantum_complexity`` -- show quantum time complexity (default: true)
+ - ``show_tilde_o_time`` -- show Ε time complexity (default: true)
+ - ``show_all_parameters`` -- show all optimization parameters (default: true)
+ - ``precision`` -- number of decimal digits output (default: 1)
+ - ``truncate`` -- truncate rather than round the output (default: false)
+
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.PEEstimator import PEEstimator
+ sage: A = PEEstimator(n=60, k=20, q=7)
+ sage: A.table(precision=3, show_all_parameters=1)
+ +-----------+-------------------------------+
+ | | estimate |
+ +-----------+---------+--------+------------+
+ | algorithm | time | memory | parameters |
+ +-----------+---------+--------+------------+
+ | Leon | 34.200 | 11.718 | {'w': 26} |
+ | Beullens | 29.631 | 11.901 | {'w': 25} |
+ | SSA | 127.480 | 14.040 | {} |
+ +-----------+---------+--------+------------+
+
+ TESTS::
+
+ sage: from cryptographic_estimators.PEEstimator import PEEstimator
+ sage: A = PEEstimator(n=200, k=100, q=51)
+ sage: A.table(precision=3, show_all_parameters=1) # long time
+ +-----------+-------------------------------+
+ | | estimate |
+ +-----------+---------+--------+------------+
+ | algorithm | time | memory | parameters |
+ +-----------+---------+--------+------------+
+ | Leon | 115.629 | 35.016 | {'w': 71} |
+ | Beullens | 99.161 | 61.851 | {'w': 85} |
+ | SSA | 587.237 | 18.377 | {} |
+ +-----------+---------+--------+------------+
+ """
+ super(PEEstimator, self).table(show_quantum_complexity=show_quantum_complexity,
+ show_tilde_o_time=show_tilde_o_time,
+ show_all_parameters=show_all_parameters,
+ precision=precision, truncate=truncate)
diff --git a/cryptographic_estimators/PEEstimator/pe_helper.py b/cryptographic_estimators/PEEstimator/pe_helper.py
new file mode 100644
index 00000000..c3afeac5
--- /dev/null
+++ b/cryptographic_estimators/PEEstimator/pe_helper.py
@@ -0,0 +1,60 @@
+from math import comb as binomial, log2, factorial
+from random import randint
+
+
+def gv_distance(n: int, k: int, q: int):
+ """
+ Gilbert Varsharmov bound
+ """
+ d = 1
+ right_term = q ** (n - k)
+ left_term = 0
+ while left_term <= right_term:
+ left_term += binomial(n, d) * (q - 1) ** d
+ d += 1
+ d = d - 1
+ return d
+
+
+def number_of_weight_d_codewords(n: int, k: int, q: int, d: int):
+ """
+ Returns the number of weight d code words in a (n,k,q) code
+ """
+ return binomial(n, d) * (q - 1) ** (d - 2) * q ** (k - n + 1) * 1.
+
+
+def random_sparse_vec_orbit(n: int, w: int, q: int):
+ """
+
+ """
+ counts = [0] * (q - 1)
+ s = 0
+ while s < w:
+ a = randint(0, q - 2)
+ s += 1
+ counts[a] += 1
+ orbit_size = factorial(n) // factorial(n - w);
+ for c in counts:
+ orbit_size //= factorial(c)
+ return log2(orbit_size)
+
+
+def median_size_of_random_orbit(n: int, w: int, q: int):
+ """
+
+ """
+ S = []
+ for x in range(100):
+ S.append(random_sparse_vec_orbit(n, w, q))
+ S.sort()
+ return S[49]
+
+
+def hamming_ball(n: int, q: int, w: int):
+ """
+
+ """
+ S = 0
+ for i in range(0, w + 1):
+ S += binomial(n, i) * (q - 1) ** i
+ return log2(S)
\ No newline at end of file
diff --git a/cryptographic_estimators/PEEstimator/pe_problem.py b/cryptographic_estimators/PEEstimator/pe_problem.py
new file mode 100644
index 00000000..43b39786
--- /dev/null
+++ b/cryptographic_estimators/PEEstimator/pe_problem.py
@@ -0,0 +1,92 @@
+# ****************************************************************************
+# 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_problem import BaseProblem
+from .pe_constants import *
+from math import log2, factorial
+
+
+class PEProblem(BaseProblem):
+ """
+ Construct an instance of the Permutation Code Equivalence Problem
+
+ INPUT:
+
+ - ``n`` -- code length
+ - ``k`` -- code dimension
+ - ``q`` -- field size
+ - ``h`` -- dimension of the hull (Default: min(n,n-k), i.e., code is assumed to be weakly self dual)
+ - ``nsolutions`` -- number of (expected) solutions of the problem in logarithmic scale
+ - ``memory_bound`` -- maximum allowed memory to use for solving the problem
+
+ """
+
+ def __init__(self, n: int, k: int, q: int, **kwargs):
+ super().__init__(**kwargs)
+ self.parameters[PE_CODE_LENGTH] = n
+ self.parameters[PE_CODE_DIMENSION] = k
+ self.parameters[PE_FIELD_SIZE] = q
+ self.parameters[PE_HULL_DIMENSION] = kwargs.get("h", min(n, n - k))
+ self.nsolutions = kwargs.get("nsolutions", max(self.expected_number_solutions(), 0))
+
+ def to_bitcomplexity_time(self, basic_operations: float):
+ """
+ Returns the bit-complexity corresponding to basic_operations Fq additions
+
+ INPUT:
+
+ - ``basic_operations`` -- Number of field additions (logarithmic)
+
+ """
+ _, _, q, _ = self.get_parameters()
+ return basic_operations + log2(log2(q))
+
+ def to_bitcomplexity_memory(self, elements_to_store: float):
+ """
+ Returns the memory bit-complexity associated to a given number of Fq elements to store
+
+ INPUT:
+
+ - ``elements_to_store`` -- number of elements to store (logarithmic)
+
+ """
+ _, _, q, _ = self.get_parameters()
+ return elements_to_store + log2(log2(q))
+
+ def expected_number_solutions(self):
+ """
+ Returns the logarithm of the expected number of existing solutions to the problem
+
+ """
+ n, k, q, _ = self.get_parameters()
+ return log2(q) * k * k + log2(factorial(n)) - log2(q) * n * k
+
+ def __repr__(self):
+ """
+ """
+ n, k, q, _ = self.get_parameters()
+ rep = "permutation equivalence problem with (n,k,q) = " \
+ + "(" + str(n) + "," + str(k) + "," + str(q) + ")"
+
+ return rep
+
+ def get_parameters(self):
+ """
+ Returns n, k, q and h
+ """
+ return self.parameters.values()
diff --git a/cryptographic_estimators/PKEstimator/PKAlgorithms/__init__.py b/cryptographic_estimators/PKEstimator/PKAlgorithms/__init__.py
new file mode 100644
index 00000000..90cbba58
--- /dev/null
+++ b/cryptographic_estimators/PKEstimator/PKAlgorithms/__init__.py
@@ -0,0 +1,2 @@
+from .kmp import KMP
+from .sbc import SBC
\ No newline at end of file
diff --git a/cryptographic_estimators/PKEstimator/PKAlgorithms/kmp.py b/cryptographic_estimators/PKEstimator/PKAlgorithms/kmp.py
new file mode 100644
index 00000000..79a735b7
--- /dev/null
+++ b/cryptographic_estimators/PKEstimator/PKAlgorithms/kmp.py
@@ -0,0 +1,89 @@
+from ..pk_algorithm import PKAlgorithm
+from ..pk_problem import PKProblem
+from ..pk_constants import *
+from ...base_algorithm import optimal_parameter
+from math import log2, factorial
+
+
+class KMP(PKAlgorithm):
+ """
+ Complexity estimate of the KMP algorithm
+
+ Originally proposed in [KMP19]_ . The estimates are adapted versions of the code accompanying [SBC22]_, original
+ code is accessible at https://github.com/secomms/pkpattack
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.PKEstimator.PKAlgorithms import KMP
+ sage: from cryptographic_estimators.PKEstimator import PKProblem
+ sage: KMP(PKProblem(n=100,m=50,q=31,ell=2))
+ KMP estimator for the permuted kernel problem with (n,m,q,ell) = (100,50,31,2)
+
+ """
+
+ def __init__(self, problem: PKProblem, **kwargs):
+ super().__init__(problem, **kwargs)
+ self._name = "KMP"
+ _, m, _, _ = self.problem.get_parameters()
+
+ self.set_parameter_ranges("u", 0, m)
+
+ @optimal_parameter
+ def u(self):
+ """
+ Return the optimal parameter $u$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.PKEstimator.PKAlgorithms import KMP
+ sage: from cryptographic_estimators.PKEstimator import PKProblem
+ sage: A = KMP(PKProblem(n=100,m=50,q=31,ell=2))
+ sage: A.u()
+ 24
+
+ """
+ return self._get_optimal_parameter("u")
+
+ def _compute_time_and_memory(self, parameters: dict, verbose_information=None):
+ """
+ Computes the time and memory complexity of the KMP algorithm in number of Fq additions and Fq elements resp.
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including parameters
+ - ``verbose_information`` -- if set to a dictionary `L1`, `L1`, and `final_list` will be returned.
+
+ """
+ u = parameters["u"]
+ n, m, q, ell = self.problem.get_parameters()
+ u1 = int((n - m + u) / 2)
+ u2 = n - m + u - u1
+
+ L1 = factorial(n) // factorial(n - u1)
+ L2 = factorial(n) // factorial(n - u2)
+ num_coll = factorial(n) * factorial(n) // factorial(n - u1) \
+ // factorial(n - u2) * q ** (ell * (n - m - u1 - u2))
+
+ time = log2(L1 + L2 + num_coll) + log2(self.cost_for_list_operation)
+ memory = log2(L1 + L2) + log2(self.memory_for_list_element)
+
+ if verbose_information is not None:
+ verbose_information[VerboseInformation.KMP_L1.value] = log2(L1)
+ verbose_information[VerboseInformation.KMP_L2.value] = log2(L2)
+ verbose_information[VerboseInformation.KMP_FINAL_LIST.value] = log2(num_coll)
+
+ return time, memory
+
+ def _compute_time_complexity(self, parameters: dict):
+ return self._compute_time_and_memory(parameters)[0]
+
+ def _compute_memory_complexity(self, parameters: dict):
+ return self._compute_time_and_memory(parameters)[1]
+
+ def _get_verbose_information(self):
+ """
+ returns a dictionary containing additional algorithm information
+ """
+ verb = dict()
+ _ = self._compute_time_and_memory(self.optimal_parameters(), verbose_information=verb)
+ return verb
diff --git a/cryptographic_estimators/PKEstimator/PKAlgorithms/sbc.py b/cryptographic_estimators/PKEstimator/PKAlgorithms/sbc.py
new file mode 100644
index 00000000..67acb970
--- /dev/null
+++ b/cryptographic_estimators/PKEstimator/PKAlgorithms/sbc.py
@@ -0,0 +1,182 @@
+from ..pk_algorithm import PKAlgorithm
+from ..pk_problem import PKProblem
+from ..pk_constants import *
+from ...base_algorithm import optimal_parameter
+from math import factorial, inf, comb as binomial, log2
+from ..pk_helper import gauss_binomial, cost_for_finding_subcode
+from ...SDFqEstimator.sdfq_estimator import SDFqEstimator
+
+
+class SBC(PKAlgorithm):
+ """
+ Complexity estimate of the SBC algorithm
+
+ The estimates are adapted versions of the code accompanying [SBC22]_, original code is accessible at
+ https://github.com/secomms/pkpattack
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.PKEstimator.PKAlgorithms import SBC
+ sage: from cryptographic_estimators.PKEstimator import PKProblem
+ sage: SBC(PKProblem(n=20,m=10,q=7,ell=2))
+ SBC estimator for the permuted kernel problem with (n,m,q,ell) = (20,10,7,2)
+
+ """
+
+ def __init__(self, problem: PKProblem, **kwargs):
+ super().__init__(problem, **kwargs)
+ self._name = "SBC"
+ n, m, _, _ = self.problem.get_parameters()
+
+ self.set_parameter_ranges("d", 1, m)
+ self.set_parameter_ranges("w", 1, n)
+ self.set_parameter_ranges("w1", 1, n)
+
+ self.SDFqEstimator = None
+ self.SDFqEstimator_parameters = kwargs.get("sd_parameters", {})
+ self.SDFqEstimator_parameters.pop("nsolutions", None)
+ self.SDFqEstimator_parameters.pop("memory_bound", None)
+ self.SDFqEstimator_parameters.pop("bit_complexities", None)
+
+ @optimal_parameter
+ def d(self):
+ """
+ Return the optimal parameter $d$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.PKEstimator.PKAlgorithms import SBC
+ sage: from cryptographic_estimators.PKEstimator import PKProblem
+ sage: A = SBC(PKProblem(n=20,m=10,q=7,ell=2))
+ sage: A.d()
+ 3
+
+ """
+ return self._get_optimal_parameter("d")
+
+ @optimal_parameter
+ def w(self):
+ """
+ Return the optimal parameter $w$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.PKEstimator.PKAlgorithms import SBC
+ sage: from cryptographic_estimators.PKEstimator import PKProblem
+ sage: A = SBC(PKProblem(n=20,m=10,q=7,ell=2))
+ sage: A.w()
+ 11
+
+ """
+ return self._get_optimal_parameter("w")
+
+ @optimal_parameter
+ def w1(self):
+ """
+ Return the optimal parameter $w1$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.PKEstimator.PKAlgorithms import SBC
+ sage: from cryptographic_estimators.PKEstimator import PKProblem
+ sage: A = SBC(PKProblem(n=20,m=10,q=7,ell=2))
+ sage: A.w1()
+ 5
+
+ """
+ return self._get_optimal_parameter("w1")
+
+ def _are_parameters_invalid(self, parameters: dict):
+ d = parameters["d"]
+ w = parameters["w"]
+ w1 = parameters["w1"]
+
+ n, m, q, ell = self.problem.get_parameters()
+
+ if w1 > w or w < d or n - w < m - d or (d == 1 and w > n - m):
+ return True
+ return False
+
+ def _compute_time_and_memory(self, parameters: dict, verbose_information=None):
+ """
+ Computes the time and memory complexity of the SBC algorithm in number of Fq additions and Fq elements resp.
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including parameters
+ - ``verbose_information`` -- if set to a dictionary `L1`, `L1`, and `final_list` will be returned.
+
+ """
+ d = parameters["d"]
+ w = parameters["w"]
+ w1 = parameters["w1"]
+
+ time = inf
+ memory = inf
+ best_u = 0
+ n, m, q, ell = self.problem.get_parameters()
+
+
+
+ N_w = log2(binomial(n, w)) + log2((q ** d - 1)) * (w - d) + gauss_binomial(m, d, q) - gauss_binomial(n, d,
+ q) # number of expected subcodes
+
+ if N_w < 0: # continue only if at least one subcode exists in expectation
+ return inf, inf
+
+ if d == 1:
+ self.SDFqEstimator = SDFqEstimator(n=n, k=m, w=w, q=q, bit_complexities=0, nsolutions=N_w,
+ memory_bound=self.problem.memory_bound, **self.SDFqEstimator_parameters)
+ c_isd = self.SDFqEstimator.fastest_algorithm().time_complexity()
+ else:
+ self.SDFqEstimator = None
+ c_isd = cost_for_finding_subcode(n, m, d, w, N_w)
+
+ w2 = w - w1
+ T_K = factorial(n) // factorial(n - w1) + factorial(n) // factorial(n - w2) \
+ + factorial(n) ** 2 // q ** (d * ell) // (factorial(n - w1) * factorial(n - w2))
+
+ if self._is_early_abort_possible(log2(T_K)):
+ return inf, inf
+ L = min(factorial(n) // factorial(n - w1), factorial(n) // factorial(n - w2))
+ size_K = max(1, factorial(n) // factorial(n - w) // q ** (d * ell))
+ for u in range(1, m):
+
+ if u > d:
+ T_L = factorial(n) // factorial(m + w - u) + size_K + factorial(n) // factorial(m + w - u) * size_K \
+ // q ** (ell * (u - d))
+ else:
+ T_L = factorial(n) // factorial(m + w - u) + size_K + factorial(n) * q ** (ell * (d - u)) // factorial(
+ m + w - u) * size_K
+ L = max(L, min(factorial(n) // factorial(m + w - u), size_K))
+
+ T_test = factorial(n - w) // q ** ((u - d) * ell) // factorial(m - u) * size_K
+
+ local_time = log2(2 ** c_isd + (T_K + T_L + T_test) * self.cost_for_list_operation)
+
+ local_memory = log2(L) + log2(self.memory_for_list_element)
+
+ if local_time < time:
+ best_u = u
+ time = local_time
+ memory = local_memory
+
+ if verbose_information is not None:
+ verbose_information[VerboseInformation.SBC_ISD] = c_isd
+ verbose_information[VerboseInformation.SBC_U] = best_u
+
+ return time, memory
+
+ def _compute_time_complexity(self, parameters: dict):
+ return self._compute_time_and_memory(parameters)[0]
+
+ def _compute_memory_complexity(self, parameters: dict):
+ return self._compute_time_and_memory(parameters)[1]
+
+ def _get_verbose_information(self):
+ """
+ returns a dictionary containing additional algorithm information
+ """
+ verb = dict()
+ _ = self._compute_time_and_memory(self.optimal_parameters(), verbose_information=verb)
+ return verb
diff --git a/cryptographic_estimators/PKEstimator/__init__.py b/cryptographic_estimators/PKEstimator/__init__.py
new file mode 100644
index 00000000..023f45b9
--- /dev/null
+++ b/cryptographic_estimators/PKEstimator/__init__.py
@@ -0,0 +1,6 @@
+from .pk_algorithm import PKAlgorithm
+from .pk_estimator import PKEstimator
+from .pk_problem import PKProblem
+from .PKAlgorithms import KMP
+from .PKAlgorithms import SBC
+# TODO: Remember to add the algorithms to the import above
\ No newline at end of file
diff --git a/cryptographic_estimators/PKEstimator/pk_algorithm.py b/cryptographic_estimators/PKEstimator/pk_algorithm.py
new file mode 100644
index 00000000..0040c40c
--- /dev/null
+++ b/cryptographic_estimators/PKEstimator/pk_algorithm.py
@@ -0,0 +1,42 @@
+# ****************************************************************************
+# 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 BaseAlgorithm
+from .pk_problem import PKProblem
+
+
+class PKAlgorithm(BaseAlgorithm):
+ def __init__(self, problem: PKProblem, **kwargs):
+ """
+ Base class for Permuted Kernel algorithms
+
+ INPUT:
+
+ - ``problem`` -- LEProblem object including all necessary parameters
+ - ``cost_for_list_operation`` -- cost in Fq additions for one list operation in the SBC and KMP algorithms (default: n-m)
+ - ``memory_for_list_element`` -- memory in Fq elements for one list element in the SBC and KMP algorithms (default: n-m)
+ """
+ super(PKAlgorithm, self).__init__(problem, **kwargs)
+ n, m, _, _ = self.problem.get_parameters()
+ self.cost_for_list_operation = kwargs.get("cost_for_list_operation", n - m)
+ self.memory_for_list_element = kwargs.get("memory_for_list_element", n - m)
+
+ if self.memory_for_list_element > self.cost_for_list_operation:
+ raise ValueError("Cost per list element must be at least as high as its memory usage")
+
+ def __repr__(self):
+ return f"{self._name} estimator for the " + str(self.problem)
diff --git a/cryptographic_estimators/PKEstimator/pk_constants.py b/cryptographic_estimators/PKEstimator/pk_constants.py
new file mode 100644
index 00000000..31fefacc
--- /dev/null
+++ b/cryptographic_estimators/PKEstimator/pk_constants.py
@@ -0,0 +1,15 @@
+from enum import Enum
+
+
+PK_COLUMNS = "columns"
+PK_ROWS = "rows"
+PK_FIELD_SIZE = "field size"
+PK_DIMENSION = "dimension"
+
+
+class VerboseInformation(Enum):
+ KMP_L1 = "L1"
+ KMP_L2 = "L2"
+ KMP_FINAL_LIST = "final list"
+ SBC_ISD = "ISD cost"
+ SBC_U = "u"
diff --git a/cryptographic_estimators/PKEstimator/pk_estimator.py b/cryptographic_estimators/PKEstimator/pk_estimator.py
new file mode 100644
index 00000000..a808e7e5
--- /dev/null
+++ b/cryptographic_estimators/PKEstimator/pk_estimator.py
@@ -0,0 +1,97 @@
+# ****************************************************************************
+# 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 ..PKEstimator.pk_algorithm import PKAlgorithm
+from ..PKEstimator.pk_problem import PKProblem
+from ..base_estimator import BaseEstimator
+from math import inf
+
+
+class PKEstimator(BaseEstimator):
+ """
+ Construct an instance of Permuted Kernel Estimator
+
+ INPUT:
+
+ - ``n`` -- columns of the matrix
+ - ``m`` -- rows of the matrix
+ - ``q`` -- size of the field
+ - ``ell`` -- rows of the matrix whose permutation should lie in the kernel
+ - ``sd_parameters`` -- Dictionary of optional parameter arguments for SDEstimator used by SBC algorithm
+ - ``cost_for_list_operation`` -- Cost in Fq additions for one list operation in the SBC and KMP algorithm (default n-m)
+ - ``memory_for_list_element`` -- Memory in Fq elements for one list element in the SBC and KMP algorithm (default n-m)
+ - ``use_parity_row`` -- enables trick of appending extra (all one) row to the matrix, i.e., m -> m+1 (default:False)
+ - ``excluded_algorithms`` -- a list/tuple of excluded algorithms (default: None)
+ - ``nsolutions`` -- no. of solutions
+
+ """
+ excluded_algorithms_by_default = []
+
+ def __init__(self, n: int, m: int, q: int, ell: int = 1, memory_bound=inf, **kwargs):
+ if not kwargs.get("excluded_algorithms"):
+ kwargs["excluded_algorithms"] = []
+
+ kwargs["excluded_algorithms"] += self.excluded_algorithms_by_default
+ super(PKEstimator, self).__init__(
+ PKAlgorithm, PKProblem(n, m, q, ell=ell, memory_bound=memory_bound, **kwargs), **kwargs)
+
+ def table(self, show_quantum_complexity=0, show_tilde_o_time=0,
+ show_all_parameters=0, precision=1, truncate=0):
+ """
+ Print table describing the complexity of each algorithm and its optimal parameters
+
+ INPUT:
+
+ - ``show_quantum_complexity`` -- show quantum time complexity (default: true)
+ - ``show_tilde_o_time`` -- show Ε time complexity (default: true)
+ - ``show_all_parameters`` -- show all optimization parameters (default: true)
+ - ``precision`` -- number of decimal digits output (default: 1)
+ - ``truncate`` -- truncate rather than round the output (default: false)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.PKEstimator import PKEstimator
+ sage: A = PKEstimator(n=40,m=10,q=7,ell=2)
+ sage: A.table()
+ +-----------+----------------+
+ | | estimate |
+ +-----------+-------+--------+
+ | algorithm | time | memory |
+ +-----------+-------+--------+
+ | KMP | 146.4 | 105.5 |
+ | SBC | 137.6 | 42.8 |
+ +-----------+-------+--------+
+
+ TESTS::
+
+ sage: from cryptographic_estimators.PKEstimator import PKEstimator
+ sage: A = PKEstimator(n=100,m=50,q=31,ell=2)
+ sage: A.table(precision=3, show_all_parameters=1) # long time
+ +-----------+------------------------------------------------+
+ | | estimate |
+ +-----------+---------+---------+----------------------------+
+ | algorithm | time | memory | parameters |
+ +-----------+---------+---------+----------------------------+
+ | KMP | 243.808 | 243.722 | {'u': 24} |
+ | SBC | 241.319 | 236.722 | {'d': 1, 'w': 38, 'w1': 2} |
+ +-----------+---------+---------+----------------------------+
+
+ """
+ super(PKEstimator, self).table(show_quantum_complexity=show_quantum_complexity,
+ show_tilde_o_time=show_tilde_o_time,
+ show_all_parameters=show_all_parameters,
+ precision=precision, truncate=truncate)
diff --git a/cryptographic_estimators/PKEstimator/pk_helper.py b/cryptographic_estimators/PKEstimator/pk_helper.py
new file mode 100644
index 00000000..a2d942b3
--- /dev/null
+++ b/cryptographic_estimators/PKEstimator/pk_helper.py
@@ -0,0 +1,28 @@
+from math import log2, comb as binomial, factorial, inf
+
+
+def gauss_binomial(m: int, r: int, q: int):
+ return log2(q) * (r * (m - r))
+
+
+def beullens_lee_brickell_adaptation(n: int, k: int, d: int, w: int, Nw: int):
+ """
+ Running time of Beullens LeeBrickel adaptation for finding d-dimensional subcode with support w
+ """
+ if w < d or n-w < k-d:
+ return inf
+ iterations = log2(binomial(n, k)) - log2(binomial(w, d)) - log2(binomial(n - w, k - d))
+ c_iter = k ** 3 + binomial(k, d)
+
+ return log2(c_iter) + iterations - Nw
+
+
+def lof(x: int):
+ return log2(factorial(x))
+
+def cost_for_finding_subcode(n: int, k: int, d: int, w: int, Nw: int):
+ """
+ Compute cost for computation of d-dimensional subcode with support w where there exist Nw of them
+ """
+ c_isd = beullens_lee_brickell_adaptation(n, k, d, w, Nw)
+ return max(0, c_isd)
diff --git a/cryptographic_estimators/PKEstimator/pk_problem.py b/cryptographic_estimators/PKEstimator/pk_problem.py
new file mode 100644
index 00000000..a871523f
--- /dev/null
+++ b/cryptographic_estimators/PKEstimator/pk_problem.py
@@ -0,0 +1,92 @@
+# ****************************************************************************
+# 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_problem import BaseProblem
+from .pk_constants import *
+from math import log2, factorial
+
+
+class PKProblem(BaseProblem):
+ """
+ Construct an instance of the Permuted Kernel Problem
+
+ INPUT:
+
+ - ``n`` -- columns of the matrix
+ - ``m`` -- rows of the matrix
+ - ``q`` -- size of the field
+ - ``ell`` -- number of rows of the matrix whose permutation should lie in the kernel (default: 1)
+ - ``use_parity_row`` -- enables trick of appending extra (all one) row to the matrix, i.e., m -> m+1 (default: false)
+ - ``nsolutions`` -- number of solutions of the problem in logarithmic scale (default: expected_number_solutions)
+
+ """
+
+
+ def __init__(self, n: int, m: int, q: int, ell=1, **kwargs):
+ super().__init__(**kwargs)
+
+ self.parameters[PK_COLUMNS] = n
+ self.parameters[PK_ROWS] = m
+ self.parameters[PK_FIELD_SIZE] = q
+ self.parameters[PK_DIMENSION] = ell
+
+ if q ** ell < n:
+ raise ValueError("q^ell should be at least n, otherwise possible number of permutations is not maximal")
+
+ self.nsolutions = kwargs.get("nsolutions", max(self.expected_number_solutions(), 0))
+ if kwargs.get("use_parity_row", False):
+ self.parameters[PK_ROWS] += 1
+
+ def to_bitcomplexity_time(self, basic_operations: float):
+ """
+ Returns the bit-complexity corresponding to basic_operations field additions
+
+ INPUT:
+
+ - ``basic_operations`` -- Number of Fq additions (logarithmic)
+
+ """
+ return basic_operations + log2(log2(self.parameters[PK_FIELD_SIZE]))
+
+ def to_bitcomplexity_memory(self, elements_to_store: float):
+ """
+ Returns the memory bit-complexity associated to a given number of elements to store
+
+ INPUT:
+
+ - ``elements_to_store`` -- number of Fq elements the algorithm needs to store (logarithmic)
+
+ """
+ return elements_to_store + log2(log2(self.parameters[PK_FIELD_SIZE]))
+
+ def expected_number_solutions(self):
+ """
+ Returns the logarithm of the expected number of existing solutions to the problem
+
+ """
+ n, m, q, ell = self.get_parameters()
+ return log2(factorial(n)) - log2(q) * m * ell
+
+ def __repr__(self):
+ n, m, q, ell = self.get_parameters()
+ rep = "permuted kernel problem with (n,m,q,ell) = " \
+ + "(" + str(n) + "," + str(m) + "," + str(q) + "," + str(ell) + ")"
+
+ return rep
+
+ def get_parameters(self):
+ return self.parameters.values()
diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/__init__.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/__init__.py
new file mode 100644
index 00000000..0f637b10
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/__init__.py
@@ -0,0 +1,10 @@
+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 BJMM_plus
+from .both_may import BothMay
+from .dumer import Dumer
+from .may_ozerov import MayOzerov, MayOzerovD2, MayOzerovD3
+from .prange import Prange
+from .stern import Stern
diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/ball_collision.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/ball_collision.py
new file mode 100644
index 00000000..f205e8db
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/ball_collision.py
@@ -0,0 +1,175 @@
+# ****************************************************************************
+# 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, binom, log2, inf, min_max
+from types import SimpleNamespace
+from ..sd_constants import *
+from ..SDWorkfactorModels.ball_collision import BallCollisionScipyModel
+
+
+class BallCollision(SDAlgorithm):
+ def __init__(self, problem: SDProblem, **kwargs):
+ """
+ Complexity estimate of the ball collision decoding algorithm
+
+ Introduced in [BLP11]_.
+
+ expected weight distribution::
+
+ +------------------+---------+---------+-------------+-------------+
+ | <-+ n - k - l +->|<- l/2 ->|<- l/2 ->|<--+ k/2 +-->|<--+ k/2 +-->|
+ | w - 2p - 2pl | pl | pl | p | p |
+ +------------------+---------+---------+-------------+-------------+
+
+ INPUT:
+
+ - ``problem`` -- SDProblem object including all necessary parameters
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BallCollision
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: BallCollision(SDProblem(n=100,k=50,w=10))
+ Ball Collision estimator for syndrome decoding problem with (n,k,w) = (100,50,10) over Finite Field of size 2
+
+ """
+ super(BallCollision, self).__init__(problem, **kwargs)
+ n, k, w = self.problem.get_parameters()
+
+ self.set_parameter_ranges("p", 0, w // 2)
+ self._name="BallCollision"
+ s = self.full_domain
+ self.set_parameter_ranges("l", 0, min_max(300, n - k, s))
+ self.set_parameter_ranges("pl", 0, min_max(10, w, s))
+ self.set_parameter_ranges("r", 0, n - k)
+
+ self.scipy_model = BallCollisionScipyModel
+
+ @optimal_parameter
+ def l(self):
+ """
+ Return the optimal parameter $l$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BallCollision
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BallCollision(SDProblem(n=100,k=50,w=10))
+ sage: A.l()
+ 7
+ """
+ return self._get_optimal_parameter("l")
+
+ @optimal_parameter
+ def p(self):
+ """
+ Return the optimal parameter $p$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BallCollision
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BallCollision(SDProblem(n=100,k=50,w=10))
+ sage: A.p()
+ 2
+ """
+ return self._get_optimal_parameter("p")
+
+ @optimal_parameter
+ def pl(self):
+ """
+ Return the optimal parameter $pl$ used in the algorithm optimization
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BallCollision
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BallCollision(SDProblem(n=100,k=50,w=10))
+ sage: A.pl()
+ 0
+
+ """
+ return self._get_optimal_parameter("pl")
+
+ 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 // 2
+ if par.p > w // 2 or k1 < par.p or par.pl > par.l // 2 or n - k - par.l < w - 2 * par.p - 2 * par.pl or w < 2 * par.p + 2 * par.pl:
+ 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()
+ start_p = new_ranges["p"]["min"]+(new_ranges["p"]["min"] % 2)
+ for p in range(start_p, 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 pl in range(new_ranges["pl"]["min"], min(l//2, new_ranges["pl"]["max"], (w-2*p)//2)+1):
+ indices = {"p": p, "pl": pl, "l": l,
+ "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 a given parameter set.
+ """
+ n, k, w = self.problem.get_parameters()
+ par = SimpleNamespace(**parameters)
+ k1 = k // 2
+
+ memory_bound = self.problem.memory_bound
+
+ L1 = binom(k1, par.p)
+ if self._is_early_abort_possible(log2(L1)):
+ return inf, inf
+
+ L1 *= max(1, binom(par.l // 2, par.pl))
+ memory = log2(2 * L1 + _mem_matrix(n, k, par.r))
+ if memory > memory_bound:
+ return inf, inf
+ solutions = self.problem.nsolutions
+ Tp = max(log2(binom(n, w)) - log2(binom(n - k - par.l, w - 2 * par.p - 2 * par.pl))
+ - 2 * log2(binom(k1, par.p)) - 2 * log2(binom(par.l // 2, par.pl)) - solutions, 0)
+ Tg = _gaussian_elimination_complexity(n, k, par.r)
+ time = Tp + log2(Tg + _list_merge_complexity(L1, par.l, self._hmap))
+
+ if verbose_information is not None:
+ verbose_information[VerboseInformation.PERMUTATIONS.value] = Tp
+ verbose_information[VerboseInformation.GAUSS.value] = log2(Tg)
+ verbose_information[VerboseInformation.LISTS.value] = [
+ log2(L1), 2 * log2(L1) - par.l]
+
+ return time, memory
+
+ def __repr__(self):
+ """
+ """
+ rep = "Ball Collision estimator for " + str(self.problem)
+ return rep
diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm.py
new file mode 100644
index 00000000..97e48ea8
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm.py
@@ -0,0 +1,556 @@
+# ****************************************************************************
+# 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 ...helper import ComplexityType
+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
+from types import SimpleNamespace
+from ..sd_constants import *
+from ..SDWorkfactorModels.bjmm import BJMMScipyModel
+from typing import Union
+
+
+class BJMM(SDAlgorithm):
+ def __init__(self, problem: SDProblem, **kwargs):
+ """
+ Complexity estimate of BJMM algorithm in depth 2,3
+
+ The algorithm was introduced in [BJMM12]_ as an extension of [MMT11]_.
+
+ 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 BJMM
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: BJMM(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(BJMM, self).__init__(problem, **kwargs)
+ self._name = "BJMM"
+ self.initialize_parameter_ranges()
+ self.limit_depth = kwargs.get("limit_depth", False)
+ self.BJMM_depth_2 = BJMMd2(problem, **kwargs)
+ self.BJMM_depth_3 = BJMMd3(problem, **kwargs)
+
+ def initialize_parameter_ranges(self):
+ """
+ initialize the parameters p, l, p1 and for d=3 p2
+ """
+ self.set_parameter_ranges("depth", 2, 3)
+
+ @optimal_parameter
+ def depth(self):
+ """
+ Return the optimal parameter $depth$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMM
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMM(SDProblem(n=100,k=50,w=10))
+ sage: A.depth()
+ 2
+ """
+ if self.complexity_type == ComplexityType.TILDEO.value:
+ return 3
+ return self._get_optimal_parameter("depth")
+
+ @property
+ def complexity_type(self):
+ """
+ Returns the complexity type.
+ """
+ return super().complexity_type
+
+ @complexity_type.setter
+ def complexity_type(self, new_type: Union[str, int]):
+ """
+ sets the complexity type.
+ """
+ super(BJMM, self.__class__).complexity_type.fset(self, new_type)
+ self.BJMM_depth_2.complexity_type = new_type
+ self.BJMM_depth_3.complexity_type = new_type
+
+ def reset(self):
+ """
+ resets all parameters to restart the optimization process.
+ """
+ super().reset()
+ self.BJMM_depth_2.reset()
+ self.BJMM_depth_3.reset()
+
+ def _find_optimal_parameters(self):
+ """
+ Finds optimal parameters for depth 2 and 3
+ """
+ self.BJMM_depth_2._find_optimal_parameters()
+ if self.limit_depth:
+ self._optimal_parameters["depth"] = 2
+ return
+
+ self.BJMM_depth_3._find_optimal_parameters()
+ if self.BJMM_depth_2.time_complexity() > self.BJMM_depth_3.time_complexity():
+ self._optimal_parameters["depth"] = 3
+ else:
+ self._optimal_parameters["depth"] = 2
+
+ def _time_and_memory_complexity(self, parameters: dict, verbose_information=None):
+ """
+ computes and returns the time and memory complexity for either the depth 2 or 3 algorithm
+
+ INPUT:
+
+ - ``parameters`` -- current parameter set
+ - ``verbose_information`` -- None or VerboseInformation
+
+ """
+ if "depth" not in parameters:
+ raise ValueError("Depth must be specified for BJMM")
+
+ if parameters["depth"] == 2 and self.BJMM_depth_2._do_valid_parameters_in_current_ranges_exist():
+ return self.BJMM_depth_2._time_and_memory_complexity(
+ self.BJMM_depth_2.optimal_parameters(), verbose_information)
+ elif parameters["depth"] == 3 and self.BJMM_depth_3._do_valid_parameters_in_current_ranges_exist():
+ return self.BJMM_depth_3._time_and_memory_complexity(
+ self.BJMM_depth_3.optimal_parameters(), verbose_information)
+ elif parameters["depth"] not in [2, 3]:
+ raise ValueError("BJMM only implemented in depth 2 and 3")
+ else:
+ return inf, inf
+
+ def _tilde_o_time_and_memory_complexity(self, parameters: dict):
+ """
+ returns the optimal time and memory complexity for BJMM d3
+ """
+
+ return self.BJMM_depth_3._tilde_o_time_and_memory_complexity(parameters)
+
+ def get_optimal_parameters_dict(self):
+ """
+ Returns the optimal parameters dictionary
+
+ """
+ a = dict()
+ a.update(self._optimal_parameters)
+ if self.depth() == 2:
+ a.update(self.BJMM_depth_2.get_optimal_parameters_dict())
+ else:
+ a.update(self.BJMM_depth_3.get_optimal_parameters_dict())
+ return a
+
+ def __repr__(self):
+ """
+ """
+ rep = "BJMM estimator for " + str(self.problem)
+ return rep
+
+
+class BJMMd2(SDAlgorithm):
+ def __init__(self, problem: SDProblem, **kwargs):
+ """
+ Complexity estimate of BJMM algorithm in depth 2
+
+ The algorithm was introduced in [BJMM12]_ as an extension of [MMT11]_.
+
+ 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 BJMM
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMM(SDProblem(n=100,k=50,w=10))
+ sage: A.BJMM_depth_2
+ BJMM estimator in depth 2 for syndrome decoding problem with (n,k,w) = (100,50,10) over Finite Field of size 2
+
+ """
+
+ super(BJMMd2, self).__init__(problem, **kwargs)
+ self._name = "BJMMd2"
+ self.initialize_parameter_ranges()
+
+ 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))
+
+ @optimal_parameter
+ def l(self):
+ """
+ Return the optimal parameter $l$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMM
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMM(SDProblem(n=100,k=50,w=10))
+ sage: A.BJMM_depth_2.l()
+ 8
+
+ """
+ return self._get_optimal_parameter("l")
+
+ @optimal_parameter
+ def p(self):
+ """
+ Return the optimal parameter $p$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMM
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMM(SDProblem(n=100,k=50,w=10))
+ sage: A.BJMM_depth_2.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 BJMM
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMM(SDProblem(n=100,k=50,w=10))
+ sage: A.BJMM_depth_2.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"]), 2):
+ for l in range(new_ranges["l"]["min"], min(n - k - (w - 2 * p), new_ranges["l"]["max"])):
+ for p1 in range(max(new_ranges["p1"]["min"], (p + 1) // 2), new_ranges["p1"]["max"]):
+ indices = {"p": p, "p1": p1, "l": l,
+ "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
+
+ solutions = self.problem.nsolutions
+ memory_bound = self.problem.memory_bound
+
+ L1 = binom(k1, par.p1)
+ if self._is_early_abort_possible(log2(L1)):
+ return inf, inf
+
+ reps = (binom(par.p, par.p / 2) *
+ binom(k1 - par.p, par.p1 - par.p / 2)) ** 2
+
+ l1 = int(ceil(log2(reps)))
+
+ if l1 > par.l:
+ return inf, inf
+
+ L12 = max(1, L1 ** 2 // 2 ** l1)
+
+ memory = log2((2 * L1 + L12) + _mem_matrix(n, k, par.r))
+ if memory > memory_bound:
+ return inf, inf
+
+ Tp = max(log2(binom(n, w)) - log2(binom(n - k - par.l, w - 2 * par.p)) - 2 * log2(
+ binom((k + par.l) // 2, par.p)) - solutions,
+ 0)
+ Tg = _gaussian_elimination_complexity(n, k, par.r)
+ T_tree = 2 * _list_merge_complexity(L1, l1, self._hmap) + \
+ _list_merge_complexity(L12, par.l - l1, self._hmap)
+ T_rep = int(ceil(2 ** (l1 - log2(reps))))
+
+ time = Tp + log2(Tg + T_rep * T_tree)
+
+ if verbose_information is not None:
+ verbose_information[VerboseInformation.CONSTRAINTS.value] = [
+ l1, par.l - 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 - l1)]
+
+ return time, memory
+
+ def __repr__(self):
+ """
+ """
+ rep = "BJMM estimator in depth 2 for " + str(self.problem)
+ return rep
+
+
+class BJMMd3(SDAlgorithm):
+ def __init__(self, problem: SDProblem, **kwargs):
+ """
+ Complexity estimate of BJMM algorithm in depth 2
+
+ The algorithm was introduced in [BJMM12]_ as an extension of [MMT11]_.
+
+ 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 BJMM
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMM(SDProblem(n=100,k=50,w=10))
+ sage: A.BJMM_depth_3
+ BJMM estimator in depth 3 for syndrome decoding problem with (n,k,w) = (100,50,10) over Finite Field of size 2
+
+ """
+
+ super(BJMMd3, self).__init__(problem, **kwargs)
+ self._name = "BJMMd3"
+ self.initialize_parameter_ranges()
+ self.scipy_model = BJMMScipyModel
+
+ def initialize_parameter_ranges(self):
+ """
+ initialize the parameter ranges for p, p1, p2, 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(25, w, s))
+ self.set_parameter_ranges("l", 0, min_max(400, n - k, s))
+ self.set_parameter_ranges("p2", 0, min_max(20, w, s))
+ self.set_parameter_ranges("p1", 0, min_max(10, w, s))
+
+ @optimal_parameter
+ def l(self):
+ """
+ Return the optimal parameter $l$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMM
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMM(SDProblem(n=100,k=50,w=10))
+ sage: A.BJMM_depth_3.l()
+ 18
+ """
+ return self._get_optimal_parameter("l")
+
+ @optimal_parameter
+ def p(self):
+ """
+ Return the optimal parameter $p$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMM
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMM(SDProblem(n=100,k=50,w=10))
+ sage: A.BJMM_depth_3.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 BJMM
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMM(SDProblem(n=100,k=50,w=10))
+ sage: A.BJMM_depth_3.p1()
+ 1
+ """
+ return self._get_optimal_parameter("p1")
+
+ @optimal_parameter
+ def p2(self):
+ """
+ Return the optimal parameter $p2$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMM
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMM(SDProblem(n=100,k=50,w=10))
+ sage: A.BJMM_depth_3.p2()
+ 2
+ """
+ return self._get_optimal_parameter("p2")
+
+ def _are_parameters_invalid(self, parameters):
+ """
+ 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.p2 - par.p / 2 or par.p2 < par.p // 2 or \
+ k1 - par.p2 < par.p1 - par.p2 / 2 or par.p1 < par.p2 / 2 or par.p % 2 == 1:
+ 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"]), 2):
+ for l in range(new_ranges["l"]["min"], min(n - k - (w - 2 * p), new_ranges["l"]["max"])):
+ for p2 in range(max(new_ranges["p2"]["min"], p // 2 + ((p // 2) % 2)), new_ranges["p2"]["max"], 2):
+ for p1 in range(max(new_ranges["p1"]["min"], (p2 + 1) // 2), new_ranges["p1"]["max"]):
+ indices = {"p": p, "p1": p1, "p2": p2,
+ "l": l, "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 3 version
+ """
+ n, k, w = self.problem.get_parameters()
+ par = SimpleNamespace(**parameters)
+
+ k1 = (k + par.l) // 2
+
+ solutions = self.problem.nsolutions
+ memory_bound = self.problem.memory_bound
+ L1 = binom(k1, par.p1)
+ if self._is_early_abort_possible(log2(L1)):
+ return inf, inf
+
+ reps1 = (binom(par.p2, par.p2 / 2) *
+ binom(k1 - par.p2, par.p1 - par.p2 / 2)) ** 2
+ l1 = int((log2(reps1))) if reps1 != 1 else 0
+
+ L12 = max(1, L1 ** 2 // 2 ** l1)
+ reps2 = (binom(par.p, par.p / 2) *
+ binom(k1 - par.p, par.p2 - par.p / 2)) ** 2
+ l2 = int(ceil(log2(reps2))) if reps2 != 1 else 0
+
+ L1234 = max(1, L12 ** 2 // 2 ** (l2 - l1))
+ memory = log2((2 * L1 + L12 + L1234) + _mem_matrix(n, k, par.r))
+ if memory > memory_bound:
+ return inf, inf
+
+ Tp = max(log2(binom(n, w)) - log2(binom(n - k - par.l, w - 2 * par.p)) - 2 * log2(
+ binom((k + par.l) // 2, par.p)) - solutions,
+ 0)
+ Tg = _gaussian_elimination_complexity(n, k, par.r)
+ T_tree = 4 * _list_merge_complexity(L1, l1, self._hmap) \
+ + 2 * _list_merge_complexity(L12, l2 - l1, self._hmap) \
+ + _list_merge_complexity(L1234, par.l - l2, self._hmap)
+ T_rep = int(
+ ceil(2 ** (3 * max(0, l1 - log2(reps1)) + max(0, l2 - log2(reps2)))))
+
+ time = Tp + log2(Tg + T_rep * T_tree)
+
+ if verbose_information is not None:
+ verbose_information[VerboseInformation.CONSTRAINTS.value] = [
+ l1, par.l - 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] = [
+ reps1, reps2]
+ verbose_information[VerboseInformation.LISTS.value] = [log2(L1), log2(L12), log2(L1234),
+ 2 * log2(L1234) - (par.l - l1 - l2)]
+ return verbose_information
+
+ return time, memory
+
+ def __repr__(self):
+ """
+ """
+ rep = "BJMM estimator in depth 3 for " + str(self.problem)
+ return rep
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..b042a1d5
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm_plus.py
@@ -0,0 +1,256 @@
+# ****************************************************************************
+# 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 BJMM_plus(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 BJMM_plus
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: BJMM_plus(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
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMM_plus
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: BJMM_plus(SDProblem(n=1284,k=1028,w=24)).time_complexity()
+ 66.34605703336426
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMM_plus
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: BJMM_plus(SDProblem(3488,2720,64)).time_complexity()
+ 142.1110119263271
+
+ """
+
+ super(BJMM_plus, 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 BJMM_plus
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMM_plus(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 BJMM_plus
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMM_plus(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 BJMM_plus
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMM_plus(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 BJMM_plus
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMM_plus(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"]), 2):
+ for l in range(new_ranges["l"]["min"], min(n - k - (w - 2 * p), new_ranges["l"]["max"])):
+ for p1 in range(max(new_ranges["p1"]["min"], (p + 1) // 2), new_ranges["p1"]["max"]):
+ L1 = int(log2(binom((k+l)//2, p1)))
+ d1 = self._adjust_radius
+ for l1 in range(max(L1-d1, 0), L1+d1):
+ 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
+ memory_bound = self.problem.memory_bound
+
+ 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
new file mode 100644
index 00000000..f114c689
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/both_may.py
@@ -0,0 +1,233 @@
+# ****************************************************************************
+# 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, _indyk_motwani_complexity, binom, \
+ log2, inf, ceil
+from types import SimpleNamespace
+from ..sd_constants import *
+from ..SDWorkfactorModels import BothMayScipyModel
+
+
+class BothMay(SDAlgorithm):
+ def __init__(self, problem: SDProblem, **kwargs):
+ """
+ Complexity estimate of Both-May algorithm in depth 2 using Indyk-Motwani and / or MitM for NN search
+
+
+ For further reference see [BM18]_.
+
+ +-------------------+---------+-------------------+-------------------+
+ | <--+ n - k - l+-->|<-+ l +->|<----+ k / 2 +---->|<----+ k / 2 +---->|
+ | w - w2 - 2p | w2 | p | p |
+ +-------------------+---------+-------------------+-------------------+
+
+ INPUT:
+
+ - ``problem`` -- SDProblem object including all necessary parameters
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BothMay
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: BothMay(SDProblem(n=100,k=50,w=10))
+ Both-May estimator in depth 2 for syndrome decoding problem with (n,k,w) = (100,50,10) over Finite Field of size 2
+
+ """
+ super(BothMay, self).__init__(problem, **kwargs)
+ self._name = "Both-May"
+ self.initialize_parameter_ranges()
+ self.scipy_model = BothMayScipyModel
+
+ def initialize_parameter_ranges(self):
+ """
+ initialize the parameter ranges for p, p1, p2, l to start the optimisation
+ process.
+ """
+ self.set_parameter_ranges("p", 0, 20)
+ self.set_parameter_ranges("p1", 0, 15)
+ self.set_parameter_ranges("l", 0, 160)
+ self.set_parameter_ranges("w1", 0, 5)
+ self.set_parameter_ranges("w2", 0, 4)
+
+ @optimal_parameter
+ def l(self):
+ """
+ Return the optimal parameter $l$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BothMay
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BothMay(SDProblem(n=100,k=50,w=10))
+ sage: A.l()
+ 2
+ """
+ return self._get_optimal_parameter("l")
+
+ @optimal_parameter
+ def p(self):
+ """
+ Return the optimal parameter $p$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BothMay
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BothMay(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 BothMay
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BothMay(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 BothMay
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BothMay(SDProblem(n=100,k=50,w=10))
+ sage: A.w1()
+ 0
+ """
+ return self._get_optimal_parameter("w1")
+
+ @optimal_parameter
+ def w2(self):
+ """
+ Return the optimal parameter $w2$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BothMay
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BothMay(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
+
+ """
+ n, k, w = self.problem.get_parameters()
+ par = SimpleNamespace(**parameters)
+ k1 = k // 2
+ if par.p > w // 2 or k1 < par.p or par.w1 >= min(w, par.l + 1) \
+ or par.w2 > min(w - 2 * par.p, par.l, 2 * par.w1) or par.p1 < (par.p + 1) // 2 or par.p1 > w \
+ or n - k - par.l < w - par.w2 - 2 * par.p or par.p1 > k1:
+ 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 w1 in range(new_ranges["w1"]["min"], new_ranges["w1"]["max"]+1):
+ for w2 in range(new_ranges["w2"]["min"], new_ranges["w2"]["max"]+1, 2):
+ for p1 in range(max(new_ranges["p1"]["min"], (p + 1) // 2), new_ranges["p1"]["max"]+1):
+ indices = {"p": p, "w1": w1, "w2": w2, "p1": p1,
+ "l": l, "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 a given parameter set.
+ """
+ n, k, w = self.problem.get_parameters()
+ par = SimpleNamespace(**parameters)
+ k1 = k // 2
+
+ solutions = self.problem.nsolutions
+ memory_bound = self.problem.memory_bound
+
+ reps = (binom(par.p, par.p / 2) * binom(k1 - par.p, par.p1 - par.p / 2)) ** 2 * binom(par.w2, par.w2 / 2) \
+ * binom(par.l - par.w2, par.w1 - par.w2 / 2)
+ reps = 1 if reps == 0 else reps
+ L1 = binom(k1, par.p1)
+
+ if self._is_early_abort_possible(log2(L1)):
+ return inf, inf
+
+ L12 = max(1, L1 ** 2 * binom(par.l, par.w1) // 2 ** par.l)
+
+ memory = log2((2 * L1 + L12) + _mem_matrix(n, k, par.r))
+ if memory > memory_bound:
+ return inf, inf
+
+ Tp = max(
+ log2(binom(n, w)) - log2(binom(n - k - par.l, w - par.w2 - 2 * par.p)) - 2 * log2(binom(k1, par.p)) - log2(
+ binom(par.l, par.w2)) - solutions, 0)
+ Tg = _gaussian_elimination_complexity(n, k, par.r)
+
+ first_level_nn = _indyk_motwani_complexity(
+ L1, par.l, par.w1, self._hmap)
+ second_level_nn = _indyk_motwani_complexity(
+ L12, n - k - par.l, w - 2 * par.p - par.w2, self._hmap)
+ T_tree = 2 * first_level_nn + second_level_nn
+ T_rep = int(ceil(2 ** max(0, par.l - log2(reps))))
+
+ time = Tp + log2(Tg + T_rep * T_tree)
+ if verbose_information is not None:
+ verbose_information[VerboseInformation.CONSTRAINTS.value] = [par.l]
+ 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)]
+
+ return time, memory
+
+ def __repr__(self):
+ """
+ """
+ rep = "Both-May estimator in depth 2 for " + str(self.problem)
+ return rep
diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/dumer.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/dumer.py
new file mode 100644
index 00000000..2b000cbd
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/dumer.py
@@ -0,0 +1,149 @@
+# ****************************************************************************
+# 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, inf
+from types import SimpleNamespace
+from ..sd_constants import *
+from ..SDWorkfactorModels.dumer import DumerScipyModel
+
+
+class Dumer(SDAlgorithm):
+ def __init__(self, problem: SDProblem, **kwargs):
+ """
+ Complexity estimate of Dumer's ISD algorithm
+
+ The algorithm was introduced in [Dum91]_.
+
+ 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 Dumer
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: Dumer(SDProblem(n=100,k=50,w=10))
+ Dumer estimator for syndrome decoding problem with (n,k,w) = (100,50,10) over Finite Field of size 2
+
+ """
+ super(Dumer, self).__init__(problem, **kwargs)
+ self._name = "Dumer"
+ self.initialize_parameter_ranges()
+ self.scipy_model = DumerScipyModel
+
+ def initialize_parameter_ranges(self):
+ """
+ initialize the parameter ranges for p, 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(w // 2, 20, s))
+ self.set_parameter_ranges("l", 0, min_max(n - k, 400, s))
+
+ @optimal_parameter
+ def l(self):
+ """
+ Return the optimal parameter $l$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import Dumer
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = Dumer(SDProblem(n=100,k=50,w=10))
+ sage: A.l()
+ 8
+ """
+ return self._get_optimal_parameter("l")
+
+ @optimal_parameter
+ def p(self):
+ """
+ Return the optimal parameter $p$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import Dumer
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = Dumer(SDProblem(n=100,k=50,w=10))
+ sage: A.p()
+ 2
+ """
+ return self._get_optimal_parameter("p")
+
+ 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 n - k - par.l < w - 2 * par.p:
+ return True
+ return False
+
+ def _time_and_memory_complexity(self, parameters, 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)
+ k1 = (k + par.l) // 2
+
+ memory_bound = self.problem.memory_bound
+
+ L1 = binom(k1, par.p)
+ if self._is_early_abort_possible(log2(L1)):
+ return inf, inf
+
+ memory = log2(2 * L1 + _mem_matrix(n, k, par.r))
+ solutions = self.problem.nsolutions
+
+ if memory > memory_bound:
+ return inf, memory_bound + 1
+
+ Tp = max(
+ log2(binom(n, w)) - log2(binom(n - k - par.l, w - 2 * par.p)) - log2(binom(k1, par.p) ** 2) - solutions, 0)
+ Tg = _gaussian_elimination_complexity(n, k, par.r)
+ time = Tp + log2(Tg + _list_merge_complexity(L1, par.l, self._hmap))
+
+ if verbose_information is not None:
+ verbose_information[VerboseInformation.PERMUTATIONS.value] = Tp
+ verbose_information[VerboseInformation.GAUSS.value] = log2(Tg)
+ verbose_information[VerboseInformation.LISTS.value] = [
+ log2(L1), 2 * log2(L1) - par.l]
+
+ return time, memory
+
+ def __repr__(self):
+ """
+ """
+ rep = "Dumer estimator for " + str(self.problem)
+ return rep
diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/may_ozerov.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/may_ozerov.py
new file mode 100644
index 00000000..42031a3e
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/may_ozerov.py
@@ -0,0 +1,540 @@
+# ****************************************************************************
+# 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 ...helper import ComplexityType
+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, \
+ _indyk_motwani_complexity, min_max, binom, log2, ceil, inf
+from types import SimpleNamespace
+from ..sd_constants import *
+from ..SDWorkfactorModels.may_ozerov import MayOzerovScipyModel
+from typing import Union
+
+
+class MayOzerov(SDAlgorithm):
+ def __init__(self, problem: SDProblem, **kwargs):
+ """
+ Complexity estimate of May-Ozerov algorithm in depth 2 and 3
+
+ Introduced in [MO15]_.
+
+ 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 MayOzerov
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: MayOzerov(SDProblem(n=100,k=50,w=10))
+ May-Ozerov estimator for syndrome decoding problem with (n,k,w) = (100,50,10) over Finite Field of size 2
+ """
+ super(MayOzerov, self).__init__(problem, **kwargs)
+ self._name = "May-Ozerov"
+ self.initialize_parameter_ranges()
+ self.limit_depth = kwargs.get("limit_depth", False)
+ self.MayOzerov_depth_2 = MayOzerovD2(problem, **kwargs)
+ self.MayOzerov_depth_3 = MayOzerovD3(problem, **kwargs)
+
+ def initialize_parameter_ranges(self):
+ self.set_parameter_ranges("depth", 2, 3)
+
+ @optimal_parameter
+ def depth(self):
+ """
+ Return the optimal parameter $depth$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMM
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = BJMM(SDProblem(n=100,k=50,w=10))
+ sage: A.depth()
+ 2
+ """
+ if self.complexity_type == ComplexityType.TILDEO.value:
+ return 3
+ return self._get_optimal_parameter("depth")
+
+ @property
+ def complexity_type(self):
+ """
+ returns the optimization type (bit security or asymptotic)
+ """
+ return super().complexity_type
+
+ @complexity_type.setter
+ def complexity_type(self, new_type: Union[str, int]):
+ """
+ sets the complexity type.
+ """
+ super(MayOzerov, self.__class__).complexity_type.fset(self, new_type)
+ self.MayOzerov_depth_2.complexity_type = new_type
+ self.MayOzerov_depth_3.complexity_type = new_type
+
+ def reset(self):
+ """
+ resets all internal variables to restart the optimization process
+ """
+ super().reset()
+ self.MayOzerov_depth_2.reset()
+ self.MayOzerov_depth_3.reset()
+
+ def _find_optimal_parameters(self):
+ """
+ Finds optimal parameters for depth 2 and 3
+ """
+ self.MayOzerov_depth_2._find_optimal_parameters()
+ if self.limit_depth:
+ self._optimal_parameters["depth"] = 2
+ return
+
+ self.MayOzerov_depth_3._find_optimal_parameters()
+ if self.MayOzerov_depth_2.time_complexity() > self.MayOzerov_depth_3.time_complexity():
+ self._optimal_parameters["depth"] = 3
+ else:
+ self._optimal_parameters["depth"] = 2
+
+ def _time_and_memory_complexity(self, parameters: dict, verbose_information=None):
+ """
+ computes and returns the time and memory complexity for either the depth 2 or 3 algorithm
+ """
+ if "depth" not in parameters:
+ raise ValueError("Depth must be specified for BJMM")
+
+ if parameters["depth"] == 2 and self.MayOzerov_depth_2._do_valid_parameters_in_current_ranges_exist():
+ return self.MayOzerov_depth_2._time_and_memory_complexity(
+ self.MayOzerov_depth_2.optimal_parameters(), verbose_information)
+ elif parameters["depth"] == 3 and self.MayOzerov_depth_3._do_valid_parameters_in_current_ranges_exist():
+ return self.MayOzerov_depth_3._time_and_memory_complexity(
+ self.MayOzerov_depth_3.optimal_parameters(), verbose_information)
+ elif parameters["depth"] not in [2, 3]:
+ raise ValueError("BJMM only implemented in depth 2 and 3")
+ else:
+ return inf, inf
+
+ def get_optimal_parameters_dict(self):
+ """
+ Returns the optimal parameters dictionary
+
+ """
+ a = dict()
+ a.update(self._optimal_parameters)
+ if self.depth() == 2:
+ a.update(self.MayOzerov_depth_2.get_optimal_parameters_dict())
+ else:
+ a.update(self.MayOzerov_depth_3.get_optimal_parameters_dict())
+ return a
+
+ def _tilde_o_time_and_memory_complexity(self, parameters: dict):
+ """
+ computes the time and memory complexity for the depth 3 algorithm
+ """
+ return self.MayOzerov_depth_3._tilde_o_time_and_memory_complexity(parameters)
+
+ def __repr__(self):
+ """
+ """
+ rep = "May-Ozerov estimator for " + str(self.problem)
+ return rep
+
+
+class MayOzerovD2(SDAlgorithm):
+ def __init__(self, problem: SDProblem, **kwargs):
+ """
+ Complexity estimate of May-Ozerov algorithm in depth 2.
+
+ [MO15] May, A., Ozerov, I.: On computing nearest neighbors with applications to decoding of binary linear codes.
+ In: Annual International Conference on the Theory and Applications of Cryptographic Techniques. pp. 203-228 . Springer (2015)
+
+ 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 MayOzerov
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = MayOzerov(SDProblem(n=100,k=50,w=10))
+ sage: A.MayOzerov_depth_2
+ May-Ozerov estimator in depth 2 for syndrome decoding problem with (n,k,w) = (100,50,10) over Finite Field of size 2
+ """
+ super(MayOzerovD2, self).__init__(problem, **kwargs)
+ self._name = "May-OzerovD2"
+ self.initialize_parameter_ranges()
+
+ def initialize_parameter_ranges(self):
+ """
+ initialize the parameters p, l, p1
+ """
+ n, k, w = self.problem.get_parameters()
+ s = self.full_domain
+ self.set_parameter_ranges("p", 0, min_max(30, w // 2, s))
+ self.set_parameter_ranges("l", 0, min_max(300, n - k, s))
+ self.set_parameter_ranges("p1", 0, min_max(25, w // 2, s))
+
+ @optimal_parameter
+ def l(self):
+ """
+ Return the optimal parameter $l$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import MayOzerov
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = MayOzerov(SDProblem(n=100,k=50,w=10))
+ sage: A.MayOzerov_depth_2.l()
+ 2
+ """
+ return self._get_optimal_parameter("l")
+
+ @optimal_parameter
+ def p(self):
+ """
+ Return the optimal parameter $p$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import MayOzerov
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = MayOzerov(SDProblem(n=100,k=50,w=10))
+ sage: A.MayOzerov_depth_2.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 MayOzerov
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = MayOzerov(SDProblem(n=100,k=50,w=10))
+ sage: A.MayOzerov_depth_2.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.l >= n - k - (w - 2 * par.p) or par.p1 < (
+ par.p + 1) // 2 or par.p > w // 2 or k1 < 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):
+ indices = {"p": p, "p1": p1, "l": l,
+ "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.
+
+ """
+ n, k, w = self.problem.get_parameters()
+ par = SimpleNamespace(**parameters)
+ k1 = (k + par.l) // 2
+
+ solutions = self.problem.nsolutions
+ memory_bound = self.problem.memory_bound
+ L1 = binom(k1, par.p1)
+
+ if self._is_early_abort_possible(log2(L1)):
+ return inf, inf
+
+ reps = (binom(par.p, par.p // 2) *
+ binom(k1 - par.p, par.p1 - par.p // 2)) ** 2
+
+ if log2(reps) > par.l + 1:
+ return inf, inf
+
+ L12 = max(1, L1 ** 2 // 2 ** par.l)
+
+ memory = log2((2 * L1 + L12) + _mem_matrix(n, k, par.r))
+ if memory > memory_bound:
+ return inf, inf
+
+ Tp = max(
+ log2(binom(n, w)) - log2(binom(n - k - par.l, w - 2 * par.p)) - 2 * log2(binom(k1, par.p)) - solutions, 0)
+ Tg = _gaussian_elimination_complexity(n, k, par.r)
+ T_tree = 2 * _list_merge_complexity(L1, par.l, self._hmap) + _indyk_motwani_complexity(L12, n - k - par.l,
+ w - 2 * par.p,
+ self._hmap)
+
+ T_rep = int(ceil(2 ** max(par.l - log2(reps), 0)))
+ time = Tp + log2(Tg + T_rep * T_tree)
+
+ if verbose_information is not None:
+ verbose_information[VerboseInformation.CONSTRAINTS.value] = [par.l]
+ 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(n - k - par.l, w - 2 * par.p)) - (
+ n - par.l)]
+
+ return time, memory
+
+ def __repr__(self):
+ """
+ """
+ rep = "May-Ozerov estimator in depth 2 for " + str(self.problem)
+ return rep
+
+
+class MayOzerovD3(SDAlgorithm):
+ def __init__(self, problem: SDProblem, **kwargs):
+ """
+ Complexity estimate of May-Ozerov algorithm in depth 3.
+
+ [MO15] May, A., Ozerov, I.: On computing nearest neighbors with applications to decoding of binary linear codes.
+ In: Annual International Conference on the Theory and Applications of Cryptographic Techniques. pp. 203-228 . Springer (2015)
+
+ 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 MayOzerov
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = MayOzerov(SDProblem(n=100,k=50,w=10))
+ sage: A.MayOzerov_depth_3
+ May-Ozerov estimator in depth 3 for syndrome decoding problem with (n,k,w) = (100,50,10) over Finite Field of size 2
+ """
+ super(MayOzerovD3, self).__init__(problem, **kwargs)
+ self._name = "May-OzerovD3"
+ self.initialize_parameter_ranges()
+ self.scipy_model = MayOzerovScipyModel
+
+ def initialize_parameter_ranges(self):
+ """
+ initialize the parameter ranges for p, p1, p2, 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(20, w // 2, s))
+ self.set_parameter_ranges("l", 0, min_max(200, n - k, s))
+ self.set_parameter_ranges("p2", 0, min_max(20, w // 2, s))
+ self.set_parameter_ranges("p1", 0, min_max(10, w // 2, s))
+
+ @optimal_parameter
+ def l(self):
+ """
+ Return the optimal parameter $l$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import MayOzerov
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = MayOzerov(SDProblem(n=100,k=50,w=10))
+ sage: A.MayOzerov_depth_3.l()
+ 11
+ """
+ return self._get_optimal_parameter("l")
+
+ @optimal_parameter
+ def p(self):
+ """
+ Return the optimal parameter $p$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import MayOzerov
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = MayOzerov(SDProblem(n=100,k=50,w=10))
+ sage: A.MayOzerov_depth_3.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 MayOzerov
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = MayOzerov(SDProblem(n=100,k=50,w=10))
+ sage: A.MayOzerov_depth_3.p1()
+ 1
+ """
+ return self._get_optimal_parameter("p1")
+
+ @optimal_parameter
+ def p2(self):
+ """
+ Return the optimal parameter $p2$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import MayOzerov
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = MayOzerov(SDProblem(n=100,k=50,w=10))
+ sage: A.MayOzerov_depth_3.p2()
+ 2
+ """
+ return self._get_optimal_parameter("p2")
+
+ 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.p2 - par.p / 2 or par.p2 < par.p / 2 or 2 * par.p > w or \
+ k1 - par.p2 < par.p1 - par.p2 / 2 or par.p1 < par.p2 / 2 or par.p % 2 != 0 or par.p2 % 2 != 0:
+ 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"]), 2):
+ for l in range(new_ranges["l"]["min"], min(n - k - (w - 2 * p), new_ranges["l"]["max"])):
+ k1 = (k+l)//2
+ for p2 in range(max(new_ranges["p2"]["min"], p // 2 + ((p // 2) % 2)), new_ranges["p2"]["max"], 2):
+ for p1 in range(max(new_ranges["p1"]["min"], (p2 + 1) // 2), min(new_ranges["p1"]["max"], k1 - p2 // 2)):
+ indices = {"p": p, "p1": p1, "p2": p2,
+ "l": l, "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
+ """
+ n, k, w = self.problem.get_parameters()
+ par = SimpleNamespace(**parameters)
+ k1 = (k + par.l) // 2
+
+ solutions = self.problem.nsolutions
+ memory_bound = self.problem.memory_bound
+ L1 = binom(k1, par.p1)
+
+ if self._is_early_abort_possible(log2(L1)):
+ return inf, inf
+
+ reps1 = (binom(par.p2, par.p2 // 2) *
+ binom(k1 - par.p2, par.p1 - par.p2 // 2)) ** 2
+ l1 = int(ceil(log2(reps1)))
+
+ if l1 > par.l:
+ return inf, inf
+ L12 = max(1, L1 ** 2 // 2 ** l1)
+ reps2 = (binom(par.p, par.p // 2) *
+ binom(k1 - par.p, par.p2 - par.p // 2)) ** 2
+
+ L1234 = max(1, L12 ** 2 // 2 ** (par.l - l1))
+ if log2(reps2) > par.l+1:
+ return inf, inf
+ memory = log2((2 * L1 + L12 + L1234) + _mem_matrix(n, k, par.r))
+ if memory > memory_bound:
+ return inf, inf
+
+ Tp = max(log2(binom(n, w)) - log2(binom(n - k - par.l, w - 2 * par.p)) - 2 * log2(binom(k1, par.p)) - solutions,
+ 0)
+ Tg = _gaussian_elimination_complexity(n, k, par.r)
+ T_tree = 4 * _list_merge_complexity(L1, l1, self._hmap) + 2 * _list_merge_complexity(L12, par.l - l1,
+ self._hmap) \
+ + _indyk_motwani_complexity(L1234, n -
+ k - par.l, w - 2 * par.p, self._hmap)
+ T_rep = int(
+ ceil(2 ** (max(par.l - log2(reps2), 0) + 3 * max(l1 - log2(reps1), 0))))
+ time = Tp + log2(Tg + T_rep * T_tree)
+
+ if verbose_information is not None:
+ verbose_information[VerboseInformation.CONSTRAINTS.value] = [
+ l1, par.l - l1]
+ verbose_information[VerboseInformation.REPRESENTATIONS.value] = Tp
+ verbose_information[VerboseInformation.TREE.value] = log2(
+ T_rep * T_tree)
+ verbose_information[VerboseInformation.GAUSS.value] = log2(Tg)
+ verbose_information[VerboseInformation.REPRESENTATIONS.value] = [
+ reps1, reps2]
+ verbose_information[VerboseInformation.LISTS.value] = [log2(L1), log2(L12), log2(L1234),
+ 2 * log2(L1234) + log2(
+ binom(n - k - par.l, w - 2 * par.p)) - (
+ n - par.l)]
+ return verbose_information
+
+ return time, memory
+
+ def __repr__(self):
+ """
+ """
+ rep = "May-Ozerov estimator in depth 3 for " + str(self.problem)
+ return rep
diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/prange.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/prange.py
new file mode 100644
index 00000000..735aed7a
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/prange.py
@@ -0,0 +1,84 @@
+# ****************************************************************************
+# 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 ...SDEstimator.sd_algorithm import SDAlgorithm
+from ...SDEstimator.sd_problem import SDProblem
+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
+
+
+class Prange(SDAlgorithm):
+ def __init__(self, problem: SDProblem, **kwargs):
+ """
+ Construct an instance of Prange's estimator [Pra62]_
+
+ expected weight distribution::
+
+ +--------------------------------+-------------------------------+
+ | <----------+ n - k +---------> | <----------+ k +------------> |
+ | w | 0 |
+ +--------------------------------+-------------------------------+
+
+ INPUT:
+
+ - ``problem`` -- SDProblem object including all necessary parameters
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import Prange
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: Prange(SDProblem(n=100,k=50,w=10))
+ Prange estimator for syndrome decoding problem with (n,k,w) = (100,50,10) over Finite Field of size 2
+ """
+ self._name = "Prange"
+ super(Prange, self).__init__(problem, **kwargs)
+ self.scipy_model = PrangeScipyModel
+
+ def _time_and_memory_complexity(self, parameters: dict, verbose_information=None):
+ """
+ Return time complexity of Prange's algorithm for given set of parameters
+
+ INPUT:
+ - ``parameters`` -- dictionary including parameters
+ - ``verbose_information`` -- if set to a dictionary `permutations` and `gauΓ` will be returned.
+ """
+
+ n, k, w = self.problem.get_parameters()
+
+ solutions = self.problem.nsolutions
+
+ r = parameters["r"]
+ memory = log2(_mem_matrix(n, k, r))
+
+ Tp = max(log2(binom(n, w)) - log2(binom(n - k, w)) - solutions, 0)
+ Tg = log2(_gaussian_elimination_complexity(n, k, r))
+ time = Tp + Tg
+
+ if verbose_information is not None:
+ verbose_information[VerboseInformation.PERMUTATIONS.value] = Tp
+ verbose_information[VerboseInformation.GAUSS.value] = Tg
+
+ return time, memory
+
+ def __repr__(self):
+ """
+ """
+ rep = "Prange estimator for " + str(self.problem)
+ return rep
diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/stern.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/stern.py
new file mode 100644
index 00000000..40560ac5
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/stern.py
@@ -0,0 +1,185 @@
+# ****************************************************************************
+# 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, binom, log2, min_max, inf
+from types import SimpleNamespace
+from ..sd_constants import *
+from ..SDWorkfactorModels.stern import SternScipyModel
+
+
+class Stern(SDAlgorithm):
+ def __init__(self, problem: SDProblem, **kwargs):
+ """
+ Construct an instance of Stern's estimator [Ste88]_, [BLP08]_.
+
+ Expected weight distribution::
+
+ +-------------------------+---------+-------------+-------------+
+ | <----+ n - k - l +----> |<-- l -->|<--+ k/2 +-->|<--+ k/2 +-->|
+ | w - 2p | 0 | p | p |
+ +-------------------------+---------+-------------+-------------+
+
+ INPUT:
+
+ - ``problem`` -- SDProblem object including all necessary parameters
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import Stern
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: Stern(SDProblem(n=100,k=50,w=10))
+ Stern estimator for syndrome decoding problem with (n,k,w) = (100,50,10) over Finite Field of size 2
+ """
+ self._name = "Stern"
+ super(Stern, self).__init__(problem, **kwargs)
+ self.initialize_parameter_ranges()
+ self.scipy_model = SternScipyModel
+
+ def initialize_parameter_ranges(self):
+ """
+ initialize the parameter ranges for p, 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(w // 2, 20, s))
+ self.set_parameter_ranges("l", 0, min_max(n - k, 400, s))
+
+ @optimal_parameter
+ def l(self):
+ """
+ Return the optimal parameter $l$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import Stern
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = Stern(SDProblem(n=100,k=50,w=10))
+ sage: A.l()
+ 9
+ """
+
+ return self._get_optimal_parameter("l")
+
+ @optimal_parameter
+ def p(self):
+ """
+ Return the optimal parameter $p$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import Stern
+ sage: from cryptographic_estimators.SDEstimator import SDProblem
+ sage: A = Stern(SDProblem(n=100,k=50,w=10))
+ sage: A.p()
+ 2
+ """
+
+ return self._get_optimal_parameter("p")
+
+ 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 // 2
+ if par.p > w // 2 or k1 < par.p or n - k - par.l < 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()
+
+ _, k, _ = self.problem.get_parameters()
+ k1 = k//2
+ for p in range(new_ranges["p"]["min"], min(k1, new_ranges["p"]["max"])+1, 2):
+ L1 = binom(k1, p)
+ l_val = int(log2(L1))
+ l_search_radius = self._adjust_radius
+ for l in range(max(new_ranges["l"]["min"], l_val-l_search_radius), min(new_ranges["l"]["max"], l_val+l_search_radius)+1):
+ indices = {"p": p, "l": l, "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):
+ """
+ Return time complexity of Sterns's algorithm for given set of parameters
+
+ INPUT:
+ - ``parameters`` -- dictionary including parameters
+ - ``verbose_information`` -- if set to a dictionary `permutations` and `gauΓ` will be returned.
+ """
+ n, k, w = self.problem.get_parameters()
+ par = SimpleNamespace(**parameters)
+ k1 = k // 2
+
+ memory_bound = self.problem.memory_bound
+
+ L1 = binom(k1, par.p)
+ if self._is_early_abort_possible(log2(L1)):
+ return inf, inf
+
+ memory = log2(2 * L1 + _mem_matrix(n, k, par.r))
+ solutions = self.problem.nsolutions
+
+ if memory > memory_bound:
+ return inf, memory_bound + 1
+
+ Tp = max(0, log2(binom(n, w)) - log2(binom(n - k, w - 2 * par.p)
+ ) - log2(binom(k1, par.p) ** 2) - solutions)
+
+ # We use Indyk-Motwani (IM) taking into account the possibility of multiple existing solutions
+ # with correct weight distribution, decreasing the amount of necessary projections
+ # remaining_sol denotes the number of expected solutions per permutation
+ # l_part_iterations is the expected number of projections need by IM to find one of those solutions
+
+ remaining_sol = (binom(n - k, w - 2 * par.p) * binom(k1, par.p) ** 2 * binom(n, w) // 2 ** (
+ n - k)) // binom(n,
+ w)
+ l_part_iterations = binom(
+ n - k, w - 2 * par.p) // binom(n - k - par.l, w - 2 * par.p)
+
+ if remaining_sol > 0:
+ l_part_iterations //= max(1, remaining_sol)
+ l_part_iterations = max(1, l_part_iterations)
+
+ Tg = _gaussian_elimination_complexity(n, k, par.r)
+ time = Tp + log2(Tg + _list_merge_complexity(L1,
+ par.l, self._hmap) * l_part_iterations)
+
+ if verbose_information is not None:
+ verbose_information[VerboseInformation.PERMUTATIONS.value] = Tp
+ verbose_information[VerboseInformation.GAUSS.value] = log2(Tg)
+ verbose_information[VerboseInformation.LISTS.value] = [
+ log2(L1), 2 * log2(L1) - par.l]
+
+ return time, memory
+
+ def __repr__(self):
+ rep = "Stern estimator for " + str(self.problem)
+ return rep
diff --git a/cryptographic_estimators/SDEstimator/SDWorkfactorModels/__init__.py b/cryptographic_estimators/SDEstimator/SDWorkfactorModels/__init__.py
new file mode 100644
index 00000000..32f10ba9
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/SDWorkfactorModels/__init__.py
@@ -0,0 +1,2 @@
+from .scipy_model import ScipyModel
+from .both_may import BothMayScipyModel
\ No newline at end of file
diff --git a/cryptographic_estimators/SDEstimator/SDWorkfactorModels/ball_collision.py b/cryptographic_estimators/SDEstimator/SDWorkfactorModels/ball_collision.py
new file mode 100644
index 00000000..4eeab108
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/SDWorkfactorModels/ball_collision.py
@@ -0,0 +1,59 @@
+# ****************************************************************************
+# 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 .
+# ****************************************************************************
+
+
+import collections
+from .scipy_model import ScipyModel
+from ..sd_problem import SDProblem
+from .workfactor_helper import representations_asymptotic, binomial_approximation, may_ozerov_near_neighbor_time
+
+
+class BallCollisionScipyModel(ScipyModel):
+ def __init__(self, par_names: list, problem: SDProblem, iterations, accuracy):
+ """
+ Optimization model for workfactor computation of Ball-Collision algorithm
+ """
+ super().__init__(par_names, problem, iterations, accuracy)
+
+ def _build_model_and_set_constraints(self):
+ self.L1 = lambda x: binomial_approximation(self.rate(x) / 2, x.p / 2) + binomial_approximation(x.l / 2, x.pl / 2)
+
+ self.constraints = [
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: self.rate(x) - x.p)},
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: x.l - x.pl)},
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: (1. - self.rate(x) - x.l) - (self.w(x) - x.p - x.pl))},
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: self.w(x) - x.p - x.pl)},
+ ]
+
+ def _memory(self, x):
+ return self.L1(x)
+
+ def _time_lists(self, x):
+ return [max(self.L1(x), 2 * self.L1(x) - x.l)]
+
+ def _time_perms(self, x):
+ return max(0,
+ binomial_approximation(1., self.w(x))
+ - binomial_approximation(self.rate(x), x.p)
+ - binomial_approximation(x.l, x.pl)
+ - binomial_approximation(1 - self.rate(x) - x.l, self.w(x) - x.p - x.pl)
+ - self.nsolutions
+ )
+
+ def _time(self, x):
+ x = self.set_vars(*x)
+ return self._time_perms(x) + max(self._time_lists(x))
diff --git a/cryptographic_estimators/SDEstimator/SDWorkfactorModels/bjmm.py b/cryptographic_estimators/SDEstimator/SDWorkfactorModels/bjmm.py
new file mode 100644
index 00000000..d84caff9
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/SDWorkfactorModels/bjmm.py
@@ -0,0 +1,106 @@
+# ****************************************************************************
+# 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 .scipy_model import ScipyModel
+from ..sd_problem import SDProblem
+from .workfactor_helper import representations_asymptotic, binomial_approximation
+from collections import namedtuple
+
+
+class BJMMScipyModel(ScipyModel):
+ def __init__(self, par_names: list, problem: SDProblem, iterations: int, accuracy: int):
+ """
+ Optimization model for workfactor computation of BJMM algorithm in depth 3
+ """
+ super().__init__(par_names, problem, iterations, accuracy)
+
+ def _build_model_and_set_constraints(self):
+ """
+ initializes the constraints for the scipy optimizer
+ """
+ self.r1 = lambda x: representations_asymptotic(
+ x.p2, x.p1 - x.p2 / 2, self.rate(x) + x.l)
+ self.r2 = lambda x: representations_asymptotic(
+ x.p, x.p2 - x.p / 2, self.rate(x) + x.l)
+
+ self.D1 = lambda x: binomial_approximation(self.rate(x) + x.l, x.p1)
+ self.D2 = lambda x: binomial_approximation(self.rate(x) + x.l, x.p2)
+
+ self.q2 = lambda x: self.D2(x) + self.r1(x) - 2 * self.D1(x)
+ self.q3 = lambda x: self.D3(x) + self.r2(x) - 2 * self.D2(x)
+
+ self.L1 = lambda x: binomial_approximation(
+ (self.rate(x) + x.l) / 2, x.p1 / 2)
+ self.L2 = lambda x: 2 * self.L1(x) - self.r1(x)
+ self.L3 = lambda x: 2 * \
+ self.L2(x) - (self.r2(x) - self.r1(x)) + self.q2(x)
+
+ self.constraints = [
+ {'type': 'ineq', 'fun': self._inject_vars(
+ lambda x: self.r2(x) - self.r1(x))},
+ {'type': 'ineq', 'fun': self._inject_vars(
+ lambda x: x.l - self.r2(x))},
+
+ {'type': 'ineq', 'fun': self._inject_vars(
+ lambda x: self.rate(x) - x.p - (x.p2 - x.p / 2))},
+ {'type': 'ineq', 'fun': self._inject_vars(
+ lambda x: self.rate(x) - x.p2 - (x.p1 - x.p2 / 2))},
+ {'type': 'ineq', 'fun': self._inject_vars(
+ lambda x: self.rate(x) - x.p1)},
+
+ {'type': 'ineq', 'fun': self._inject_vars(
+ lambda x: (1. - self.rate(x) - x.l) - (self.w(x) - x.p))},
+ {'type': 'ineq', 'fun': self._inject_vars(
+ lambda x: self.w(x) - x.p)},
+ ]
+
+ def _memory(self, x):
+ """
+ max memory = max over each list
+ """
+ return max(self.L1(x), self.L2(x), self.L3(x))
+
+ def _time_lists(self, x: float):
+ """
+ time to construct each list = max(size input list, size output list)
+ """
+ time_list1 = max(self.L1(x), 2 * self.L1(x) - self.r1(x))
+ time_list2 = max(self.L2(x), 2 * self.L2(x) -
+ (self.r2(x) - self.r1(x)))
+ time_list3 = max(self.L3(x), 2 * self.L3(x) - (x.l - self.r2(x)))
+
+ return time_list1, time_list2, time_list3
+
+ def _time_perms(self, x):
+ """
+ number of expected permutations needed.
+ """
+ return max(0,
+ binomial_approximation(1., self.w(x))
+ - binomial_approximation(self.rate(x) + x.l, x.p)
+ - binomial_approximation(1 -
+ self.rate(x) - x.l, self.w(x) - x.p)
+ - self.nsolutions
+ )
+
+ def _time(self, x):
+ """
+ total runtime of the BJMM algorithm for the given configuration
+ """
+ x = self.set_vars(*x)
+ return self._time_perms(x) + max(self._time_lists(x))
diff --git a/cryptographic_estimators/SDEstimator/SDWorkfactorModels/both_may.py b/cryptographic_estimators/SDEstimator/SDWorkfactorModels/both_may.py
new file mode 100644
index 00000000..21582482
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/SDWorkfactorModels/both_may.py
@@ -0,0 +1,74 @@
+# ****************************************************************************
+# 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 .
+# ****************************************************************************
+
+
+import collections
+from .scipy_model import ScipyModel
+from ..sd_problem import SDProblem
+from .workfactor_helper import representations_asymptotic, binomial_approximation, may_ozerov_near_neighbor_time
+
+
+class BothMayScipyModel(ScipyModel):
+ def __init__(self, par_names: list, problem: SDProblem, iterations, accuracy):
+ """
+ Optimization model for workfactor computation of Both-May algorithm in depth 2 using May-Ozerov nearest neighbor search
+ """
+ super().__init__(par_names, problem, iterations, accuracy)
+
+ def _build_model_and_set_constraints(self):
+ self.r1 = lambda x: representations_asymptotic(x.p, x.p1 - x.p / 2, self.rate(x))
+ self.c1 = lambda x: x.l - representations_asymptotic(x.w2, x.w1 - x.w2 / 2, x.l)
+
+ self.L1 = lambda x: binomial_approximation((self.rate(x)) / 2, x.p1 / 2.0)
+ self.L2 = lambda x: binomial_approximation(self.rate(x), x.p1) - x.l + binomial_approximation(x.l, x.w1)
+
+ self.constraints = [
+ {'type': 'eq', 'fun': self._inject_vars(lambda x: self.r1(x) - self.c1(x))},
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: (1 - self.rate(x) - x.l) - (self.w(x) - x.p - x.w2))},
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: 1 - self.rate(x) - x.l)},
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: self.w(x) - x.p - x.w2)},
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: x.l - x.w2 - (x.w1 - x.w2 / 2))},
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: x.l - x.w1)},
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: self.rate(x) - x.p - (x.p1 - x.p / 2))},
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: self.rate(x) - x.p1)},
+
+ ]
+
+ def _memory(self, x):
+ return max(self.L1(x), self.L2(x))
+
+ def _time_lists(self, x):
+ time_list1 = may_ozerov_near_neighbor_time(self.L1(x), x.l, x.w1)
+ time_list2 = may_ozerov_near_neighbor_time(self.L2(x), 1 - self.rate(x) - x.l, self.w(x) - x.p - x.w2)
+
+ return time_list1, time_list2
+
+ def _time_perms(self, x):
+ return max(0,
+ binomial_approximation(1, self.w(x))
+ - binomial_approximation(1 - self.rate(x) - x.l, self.w(x) - x.p - x.w2)
+ - binomial_approximation(self.rate(x), x.p)
+ - binomial_approximation(x.l, x.w2)
+ - self.nsolutions
+ )
+
+ def _time(self, x):
+ x = self.set_vars(*x)
+ perms = self._time_perms(x)
+ time_list1, time_list2 = self._time_lists(x)
+
+ return perms + max(time_list1, time_list2)
diff --git a/cryptographic_estimators/SDEstimator/SDWorkfactorModels/dumer.py b/cryptographic_estimators/SDEstimator/SDWorkfactorModels/dumer.py
new file mode 100644
index 00000000..ef9fc0e7
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/SDWorkfactorModels/dumer.py
@@ -0,0 +1,57 @@
+# ****************************************************************************
+# 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 .
+# ****************************************************************************
+
+
+import collections
+from .scipy_model import ScipyModel
+from ..sd_problem import SDProblem
+from .workfactor_helper import representations_asymptotic, binomial_approximation, may_ozerov_near_neighbor_time
+
+
+class DumerScipyModel(ScipyModel):
+ def __init__(self, par_names: list, problem: SDProblem, iterations, accuracy):
+ """
+ Optimization model for workfactor computation of Dumer's algorithm
+ """
+ super().__init__(par_names, problem, iterations, accuracy)
+
+ def _build_model_and_set_constraints(self):
+ self.L1 = lambda x: binomial_approximation((self.rate(x) + x.l) / 2, x.p / 2)
+
+ self.constraints = [
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: self.rate(x) + x.l - x.p)},
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: (1. - self.rate(x) - x.l) - (self.w(x) - x.p))},
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: self.w(x) - x.p)},
+ ]
+
+ def _memory(self, x):
+ return self.L1(x)
+
+ def _time_lists(self, x):
+ return [max(self.L1(x), 2 * self.L1(x) - x.l)]
+
+ def _time_perms(self, x):
+ return max(0,
+ binomial_approximation(1., self.w(x))
+ - binomial_approximation(self.rate(x) + x.l, x.p)
+ - binomial_approximation(1 - self.rate(x) - x.l, self.w(x) - x.p)
+ - self.nsolutions
+ )
+
+ def _time(self, x):
+ x = self.set_vars(*x)
+ return self._time_perms(x) + max(self._time_lists(x))
diff --git a/cryptographic_estimators/SDEstimator/SDWorkfactorModels/may_ozerov.py b/cryptographic_estimators/SDEstimator/SDWorkfactorModels/may_ozerov.py
new file mode 100644
index 00000000..f9ebdbf0
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/SDWorkfactorModels/may_ozerov.py
@@ -0,0 +1,78 @@
+# ****************************************************************************
+# 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 .
+# ****************************************************************************
+
+
+import collections
+from .scipy_model import ScipyModel
+from ..sd_problem import SDProblem
+from .workfactor_helper import representations_asymptotic, binomial_approximation, may_ozerov_near_neighbor_time
+
+
+class MayOzerovScipyModel(ScipyModel):
+ def __init__(self, par_names: list, problem: SDProblem, iterations, accuracy):
+ """
+ Optimization model for workfactor computation of May-Ozerov algorithm in depth 3 using May-Ozerov nearest neighbor search
+ """
+ super().__init__(par_names, problem, iterations, accuracy)
+
+ def _build_model_and_set_constraints(self):
+ self.r1 = lambda x: representations_asymptotic(x.p2, x.p1 - x.p2 / 2, self.rate(x) + x.l)
+ self.r2 = lambda x: representations_asymptotic(x.p, x.p2 - x.p / 2, self.rate(x) + x.l)
+
+ self.D1 = lambda x: binomial_approximation(self.rate(x) + x.l, x.p1)
+ self.D2 = lambda x: binomial_approximation(self.rate(x) + x.l, x.p2)
+
+ self.q2 = lambda x: self.D2(x) + self.r1(x) - 2 * self.D1(x)
+ self.q3 = lambda x: self.D3(x) + self.r2(x) - 2 * self.D2(x)
+
+ self.L1 = lambda x: binomial_approximation((self.rate(x) + x.l) / 2, x.p1 / 2)
+ self.L2 = lambda x: 2 * self.L1(x) - self.r1(x)
+ self.L3 = lambda x: 2 * self.L2(x) - (x.l - self.r1(x)) + self.q2(x)
+
+ self.constraints = [
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: self.r2(x) - self.r1(x))},
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: self.r2(x) - x.l)},
+
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: self.rate(x) - x.p - (x.p2 - x.p / 2))},
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: self.rate(x) - x.p2 - (x.p1 - x.p2 / 2))},
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: self.rate(x) - x.p1)},
+
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: (1. - self.rate(x) - x.l) - (self.w(x) - x.p))},
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: self.w(x) - x.p)},
+ ]
+
+ def _memory(self, x):
+ return max(self.L1(x), self.L2(x), self.L3(x))
+
+ def _time_lists(self, x):
+ time_list1 = max(self.L1(x), 2 * self.L1(x) - self.r1(x))
+ time_list2 = max(self.L2(x), 2 * self.L2(x) - (x.l - self.r1(x)))
+ time_list3 = may_ozerov_near_neighbor_time(self.L3(x), 1 - self.rate(x) - x.l, self.w(x) - x.p)
+
+ return time_list1, time_list2, time_list3
+
+ def _time_perms(self, x):
+ return max(0,
+ binomial_approximation(1., self.w(x))
+ - binomial_approximation(self.rate(x) + x.l, x.p)
+ - binomial_approximation(1 - self.rate(x) - x.l, self.w(x) - x.p)
+ - self.nsolutions
+ )
+
+ def _time(self, x):
+ x = self.set_vars(*x)
+ return self._time_perms(x) + max(self._time_lists(x))
diff --git a/cryptographic_estimators/SDEstimator/SDWorkfactorModels/prange.py b/cryptographic_estimators/SDEstimator/SDWorkfactorModels/prange.py
new file mode 100644
index 00000000..dff0c7ef
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/SDWorkfactorModels/prange.py
@@ -0,0 +1,56 @@
+# ****************************************************************************
+# 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 .
+# ****************************************************************************
+
+
+import collections
+from .scipy_model import ScipyModel
+from ..sd_problem import SDProblem
+from .workfactor_helper import representations_asymptotic, binomial_approximation, may_ozerov_near_neighbor_time
+
+
+class PrangeScipyModel(ScipyModel):
+ def __init__(self, par_names: list, problem: SDProblem, iterations, accuracy):
+ """
+ Optimization model for workfactor computation of Prange's algorithm
+ """
+ par_names += ["p"]
+ super().__init__(par_names, problem, iterations, accuracy)
+
+ def _build_model_and_set_constraints(self):
+ self.constraints = [
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: 1 - self.rate(x) - self.w(x) - x.p)},
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: self.rate(x) - x.p)},
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: self.w(x) - x.p)},
+ ]
+
+ def _memory(self, x):
+ return 0
+
+ def _time_lists(self, x):
+ return [binomial_approximation(self.rate(x), x.p)]
+
+ def _time_perms(self, x):
+ return max(0,
+ binomial_approximation(1., self.w(x))
+ - binomial_approximation(self.rate(x), x.p)
+ - binomial_approximation(1 - self.rate(x), self.w(x) - x.p)
+ - self.nsolutions
+ )
+
+ def _time(self, x):
+ x = self.set_vars(*x)
+ return self._time_perms(x) + max(self._time_lists(x))
diff --git a/cryptographic_estimators/SDEstimator/SDWorkfactorModels/scipy_model.py b/cryptographic_estimators/SDEstimator/SDWorkfactorModels/scipy_model.py
new file mode 100644
index 00000000..96a59d53
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/SDWorkfactorModels/scipy_model.py
@@ -0,0 +1,104 @@
+# ****************************************************************************
+# 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 .
+# ****************************************************************************
+
+
+import collections
+from ..sd_estimator import SDProblem
+from .workfactor_helper import list_of_random_tuples, wrap, binomial_approximation
+import scipy.optimize as opt
+from math import log2, inf
+
+
+class ScipyModel:
+ def __init__(self, var_names: list, problem: SDProblem, iterations, accuracy):
+ self.parameters_names = [i for i in var_names if i != 'r']
+ self.number_of_variables = len(self.parameters_names)
+ self.accuracy = accuracy
+ self.iterations = iterations
+
+ n, k, w = problem.get_parameters()
+ self.rate = lambda x: k / n
+ self.w = lambda x: w / n
+
+ self.set_vars = collections.namedtuple('SciOptModel', ' '.join(self.parameters_names))
+
+ if problem.nsolutions == max(0, problem.expected_number_solutions()):
+ self.nsolutions = max(0, binomial_approximation(1, w / n) - (1 - k / n))
+ else:
+ self.nsolutions = log2(problem.nsolutions) / n
+
+ def _inject_vars(self, f):
+ return wrap(f, self.set_vars)
+
+ def _set_bounds(self, parameters):
+ if parameters is None:
+ return [(0, 1)] * self.num_vars
+ else:
+ bounds = []
+ for i in self.parameters_names:
+ if i in parameters:
+ bounds += [(max(parameters[i] - self.accuracy, 0), parameters[i] + self.accuracy)]
+ else:
+ bounds += [(0, 1)]
+ return bounds
+
+ def _time(self, x):
+ raise NotImplementedError
+
+ def _memory(self, x):
+ raise NotImplementedError
+
+ def _optimize(self, parameters):
+ start = list_of_random_tuples(0.001, 0.01, self.number_of_variables)
+ bounds = self._set_bounds(parameters)
+
+ result = opt.minimize(
+ self._time,
+ start,
+ bounds=bounds,
+ tol=self.accuracy,
+ constraints=self.constraints,
+ options={"maxiter": 150},
+ )
+ return result
+
+ def _get_parameters(self, x):
+ par = {}
+ par_index=0
+ for par_name in self.parameters_names:
+ par[par_name] = x[par_index]
+ par_index+=1
+ return par
+
+ def get_time_memory_and_parameters(self, parameters=None):
+ self._build_model_and_set_constraints()
+
+ result = self._optimize(parameters)
+
+ for _ in range(self.iterations - 1):
+ tmp = self._optimize(parameters)
+ if tmp.success and (tmp.fun < result.fun or not result.success):
+ result = tmp
+
+ x = self.set_vars(*result.x)
+ if not result.success:
+ return inf, inf, {}
+
+ return self._time(x), self._memory(x), self._get_parameters(x)
+
+ def _build_model_and_set_constraints(self):
+ raise NotImplementedError
diff --git a/cryptographic_estimators/SDEstimator/SDWorkfactorModels/stern.py b/cryptographic_estimators/SDEstimator/SDWorkfactorModels/stern.py
new file mode 100644
index 00000000..175e756d
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/SDWorkfactorModels/stern.py
@@ -0,0 +1,57 @@
+# ****************************************************************************
+# 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 .
+# ****************************************************************************
+
+
+import collections
+from .scipy_model import ScipyModel
+from ..sd_problem import SDProblem
+from .workfactor_helper import representations_asymptotic, binomial_approximation, may_ozerov_near_neighbor_time
+
+
+class SternScipyModel(ScipyModel):
+ def __init__(self, par_names: list, problem: SDProblem, iterations, accuracy):
+ """
+ Optimization model for workfactor computation of Stern's algorithm
+ """
+ super().__init__(par_names, problem, iterations, accuracy)
+
+ def _build_model_and_set_constraints(self):
+ self.L1 = lambda x: binomial_approximation(self.rate(x) / 2, x.p / 2)
+
+ self.constraints = [
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: self.rate(x) - x.p)},
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: (1. - self.rate(x) - x.l) - (self.w(x) - x.p))},
+ {'type': 'ineq', 'fun': self._inject_vars(lambda x: self.w(x) - x.p)},
+ ]
+
+ def _memory(self, x):
+ return self.L1(x)
+
+ def _time_lists(self, x):
+ return [max(self.L1(x), 2 * self.L1(x) - x.l)]
+
+ def _time_perms(self, x):
+ return max(0,
+ binomial_approximation(1., self.w(x))
+ - binomial_approximation(self.rate(x), x.p)
+ - binomial_approximation(1 - self.rate(x) - x.l, self.w(x) - x.p)
+ - self.nsolutions
+ )
+
+ def _time(self, x):
+ x = self.set_vars(*x)
+ return self._time_perms(x) + max(self._time_lists(x))
diff --git a/cryptographic_estimators/SDEstimator/SDWorkfactorModels/workfactor_helper.py b/cryptographic_estimators/SDEstimator/SDWorkfactorModels/workfactor_helper.py
new file mode 100644
index 00000000..0a521071
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/SDWorkfactorModels/workfactor_helper.py
@@ -0,0 +1,112 @@
+# ****************************************************************************
+# 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 random import uniform as ru
+from math import log2
+from scipy.optimize import fsolve
+from typing import Any
+
+
+def inverse_binary_entropy(v: float):
+ """
+ compute the inverse binary entropy function:
+ eg the unique x in [0, ..., 1/2], v = H^{-1}(x)
+ """
+ if v == 1:
+ return 0.5
+ if v < 0.00001:
+ return 0
+
+ return fsolve(lambda x: v - (-x * log2(x) - (1 - x) * log2(1 - x)), 0.0000001)[0]
+
+
+def binary_entropy(c: float):
+ """
+ computes the binary entropy function H
+ """
+ if c == 0. or c == 1.:
+ return 0.
+
+ if c < 0. or c > 1.:
+ return -1000
+
+ return -(c * log2(c) + (1 - c) * log2(1 - c))
+
+
+def binomial_approximation(n: float, k: float):
+ """
+ computes the binomial coefficietn (n over k) via Sterlings approximation
+ """
+ if k > n or n == 0:
+ return 0
+ if k == n:
+ return 0
+ return n * binary_entropy(k / n)
+
+
+def wrap(f, g):
+ """
+ helper function for the scipy optimization framework
+ """
+ def inner(x):
+ return f(g(*x))
+
+ return inner
+
+
+def list_of_random_tuples(x: float, y: float, z: int):
+ """
+ """
+ return [(ru(x, y)) for _ in range(z)]
+
+
+def may_ozerov_near_neighbor_time(list_size: float, vector_length: float, target_weight: float):
+ """
+ computes the asymptotic runtime of the Nearest Neighbour Algorithm by
+ May-Ozerov [MO15]_
+ """
+ if vector_length <= 0 or list_size < 0:
+ return 100
+ normed_list_size = list_size / vector_length
+ if normed_list_size > 0.999999999:
+ normed_list_size = 0.999999999
+
+ normed_weight = target_weight / vector_length
+ if normed_weight > 0.5:
+ normed_weight = 1 - normed_weight
+
+ d = inverse_binary_entropy(1 - normed_list_size)
+
+ if normed_weight <= 2 * d * (1 - d):
+ mo_exp = (1 - normed_weight) * (1 -
+ binary_entropy((d - normed_weight / 2) / (1 - normed_weight)))
+ else:
+ mo_exp = 2 * normed_list_size + binary_entropy(normed_weight) - 1
+ return max(mo_exp * vector_length, 2 * list_size - vector_length + binomial_approximation(vector_length, target_weight))
+
+
+def representations_asymptotic(target_weight: float, weight_to_cancel: float, vector_length: float):
+ """
+ computes the asymptotic number of representations of a length-$vector_length$ weight-$target_weight$ vector
+ via the sum of two length-$vector_length$ weight-($target_weight$/2+$weight_to_cancel$) vectors
+ """
+ if target_weight == 0. or vector_length == 0.:
+ return 0
+ if vector_length < target_weight or vector_length - target_weight < weight_to_cancel:
+ return 0.
+ return binomial_approximation(target_weight, target_weight / 2.) + binomial_approximation(vector_length - target_weight, weight_to_cancel)
diff --git a/cryptographic_estimators/SDEstimator/__init__.py b/cryptographic_estimators/SDEstimator/__init__.py
new file mode 100644
index 00000000..0eb92aeb
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/__init__.py
@@ -0,0 +1,5 @@
+from .sd_algorithm import SDAlgorithm
+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, BJMMdw, BJMMpdw, BJMM_plus, BothMay, Dumer, MayOzerov, Prange, Stern
diff --git a/cryptographic_estimators/SDEstimator/sd_algorithm.py b/cryptographic_estimators/SDEstimator/sd_algorithm.py
new file mode 100644
index 00000000..863697a4
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/sd_algorithm.py
@@ -0,0 +1,223 @@
+# ****************************************************************************
+# 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 ..helper import ComplexityType
+from ..base_algorithm import BaseAlgorithm, optimal_parameter
+from ..SDEstimator.sd_helper import _optimize_m4ri
+from .sd_problem import SDProblem
+from math import log2, inf
+
+
+class SDAlgorithm(BaseAlgorithm):
+ def __init__(self, problem: SDProblem, **kwargs):
+ """
+ Base class for Syndrome Decoding algorithms complexity estimator
+
+ INPUT:
+
+ - ``problem`` -- SDProblem object including all necessary parameters
+ - ``var_ranges`` -- allow parameter optimization to adapt ranges if necessary (default: true)
+ - ``hmap`` -- indicates if hashmap is being used for linear time sorting (default: true)
+
+ """
+ super(SDAlgorithm, self).__init__(problem, **kwargs)
+ self._variable_parameter_ranges = kwargs.get("var_ranges", 1)
+ self._hmap = kwargs.get("hmap", 1)
+ self._adjust_radius = kwargs.get("adjust_radius", 10)
+ self.workfactor_accuracy = kwargs.get("workfactor_accuracy", 1)
+ self.scipy_model = None
+ self.full_domain = kwargs.get("full_domain", False)
+ self._current_minimum_for_early_abort = inf
+ n, k, _ = self.problem.get_parameters()
+ self.set_parameter_ranges("r", 0, n - k)
+
+ @optimal_parameter
+ def r(self):
+ """
+ Return the optimal parameter $r$ used in the optimization of the M4RI Gaussian elimination
+
+ """
+
+ if self._optimal_parameters.get("r") is None:
+ n = self.problem.parameters["code length"]
+ k = self.problem.parameters["code dimension"]
+ if self.complexity_type == ComplexityType.ESTIMATE.value:
+ return _optimize_m4ri(n, k, self.problem.memory_bound - log2(n - k))
+ elif self.complexity_type == ComplexityType.TILDEO.value:
+ return 0
+
+ return self._optimal_parameters.get("r")
+
+ def _are_parameters_invalid(self, parameters: dict):
+ """
+ returns `true` if `parameters` is an invalid parameter set
+ """
+ raise NotImplementedError
+
+ def _find_optimal_parameters(self):
+ """
+ Enumerates over all valid parameter configurations withing the ranges
+ of the optimization and saves the best result in `self._optimal_parameter`
+ """
+ _ = self.r()
+ time = inf
+ while True:
+ stop = True
+ for params in self._valid_choices():
+ if self._are_parameters_invalid(params):
+ continue
+ tmp_time, tmp_memory = self._time_and_memory_complexity(params)
+
+ if self.bit_complexities:
+ tmp_memory = self.problem.to_bitcomplexity_memory(
+ tmp_memory)
+
+ tmp_time += self.memory_access_cost(tmp_memory)
+
+ if tmp_time < time and tmp_memory < self.problem.memory_bound:
+ time, memory = tmp_time, tmp_memory
+ self._current_minimum_for_early_abort = tmp_time
+
+ for i in params:
+ self._optimal_parameters[i] = params[i]
+
+ if self._variable_parameter_ranges and len(self._optimal_parameters) > 1:
+ stop = self._adjust_parameter_ranges()
+
+ if stop:
+ break
+ self._current_minimum_for_early_abort = inf
+
+ def _find_optimal_tilde_o_parameters(self):
+ """
+ Enumerates all valid parameter within the given ranges to find the optimal one asymptotically.
+ Calls the C interface.
+ """
+ self._tilde_o_time_and_memory_complexity(self._optimal_parameters)
+
+ def _adjust_parameter_ranges(self):
+ """
+ Readjust the boundaries of the `ESTIMATE` optimization routine if the
+ optimization detects that it runs into one or more of the boundaries,
+ these boundaries will be increased/decreased by `self._adjust_radius`.
+ """
+ kept_old_ranges = True
+ r = self._adjust_radius
+
+ for i in self._optimal_parameters_methods:
+ ranges = self._parameter_ranges[i.__name__]
+ current_min = ranges["min"]
+ current_max = ranges["max"]
+ val = i()
+ if val > ranges["max"] - r:
+ ranges["max"] += r
+ ranges["min"] = max(0, min(val - r, ranges["min"] + r))
+ kept_old_ranges = False
+
+ if i() < ranges["min"] + r:
+ ranges["min"] -= r
+ ranges["min"] = max(0, ranges["min"])
+ ranges["max"] = max(val + r, ranges["max"] - r)
+ kept_old_ranges = False
+
+ if current_min == ranges["min"] and current_max == ranges["max"]:
+ kept_old_ranges = True
+ return kept_old_ranges
+
+ def _time_and_memory_complexity(self, parameters: dict, verbose_information=None):
+ """
+ Computes time and memory complexity for given parameters
+ """
+ raise NotImplementedError
+
+ def _compute_time_complexity(self, parameters: dict):
+ """
+ Compute and return the time complexity either in the asymptotic case or for
+ real parameters.
+
+ INPUT:
+ - ``parameters`` -- dictionary of parameters used for time complexity computation
+
+ """
+ return self._time_and_memory_complexity(parameters)[0]
+
+ def _compute_tilde_o_time_complexity(self, parameters: dict):
+ """
+ Compute and return the time complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+ """
+ return self._tilde_o_time_and_memory_complexity(parameters)[0]
+
+ def _compute_memory_complexity(self, parameters: dict):
+ """
+ Compute and return time complexity of the algorithm for given parameter set
+
+ INPUT:
+ - ``parameters`` -- dictionary of parameters used for time complexity computation
+
+ """
+ return self._time_and_memory_complexity(parameters)[1]
+
+ def _compute_tilde_o_memory_complexity(self, parameters: dict):
+ """
+ Compute and return time complexity the algorithm for given parameter set
+
+ INPUT:
+ - ``parameters`` -- dictionary of parameters used for time complexity computation
+
+ """
+ return self._tilde_o_time_and_memory_complexity(parameters)[1]
+
+ def _tilde_o_time_and_memory_complexity(self, parameters: dict):
+ """
+ Computes and returns time and memory complexity of the algorithm for given parameter set
+
+ INPUT:
+ - ``parameters`` -- dictionary of parameters used for time complexity computation
+
+ """
+ if self.scipy_model is None:
+ raise NotImplementedError(
+ "For " + self._name + " TildeO complexity is not yet implemented")
+ model = self.scipy_model(self.parameter_names(
+ ), self.problem, iterations=self.workfactor_accuracy*10, accuracy=1e-7)
+ wf_time, wf_memory, par = model.get_time_memory_and_parameters(
+ parameters=parameters)
+ self._optimal_parameters.update(par)
+ n, _, _ = self.problem.get_parameters()
+ return wf_time*n, wf_memory*n
+
+ def _get_verbose_information(self):
+ """
+ returns a dictionary containing
+ {
+ CONSTRAINTS,
+ PERMUTATIONS,
+ TREE,
+ GAUSS,
+ REPRESENTATIONS,
+ LISTS
+ }
+ """
+ verb = dict()
+ _ = self._time_and_memory_complexity(
+ self.optimal_parameters(), verbose_information=verb)
+ return verb
diff --git a/cryptographic_estimators/SDEstimator/sd_constants.py b/cryptographic_estimators/SDEstimator/sd_constants.py
new file mode 100644
index 00000000..72fdc5e2
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/sd_constants.py
@@ -0,0 +1,34 @@
+# ****************************************************************************
+# 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 enum import Enum
+
+SD_CODE_LENGTH = "code length"
+SD_CODE_DIMENSION = "code dimension"
+SD_ERROR_WEIGHT = "error weight"
+
+
+class VerboseInformation(Enum):
+ """
+ """
+ CONSTRAINTS = "constraints"
+ PERMUTATIONS = "permutations"
+ TREE = "tree"
+ GAUSS = "gauss"
+ REPRESENTATIONS = "representation"
+ LISTS = "lists"
diff --git a/cryptographic_estimators/SDEstimator/sd_estimator.py b/cryptographic_estimators/SDEstimator/sd_estimator.py
new file mode 100644
index 00000000..ea0a7dd3
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/sd_estimator.py
@@ -0,0 +1,154 @@
+# ****************************************************************************
+# 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 ..SDEstimator.sd_algorithm import SDAlgorithm
+from ..SDEstimator.sd_problem import SDProblem
+from ..SDEstimator.SDAlgorithms import BJMMd2, BJMMd3, MayOzerovD2, MayOzerovD3
+from ..base_estimator import BaseEstimator
+from math import inf
+
+
+class SDEstimator(BaseEstimator):
+ """
+ Construct an instance of Syndrome Decoding Estimator
+
+ INPUT:
+
+ - ``n`` -- code length
+ - ``k`` -- code dimension
+ - ``w`` -- error weight
+ - ``excluded_algorithms`` -- a list/tuple of excluded algorithms (default: None)
+ - ``nsolutions`` -- no. of solutions
+
+ TODO: Maybe we should add the optional_parameters dictionary here?
+
+ """
+
+ excluded_algorithms_by_default = [BJMMd2, BJMMd3, MayOzerovD2, MayOzerovD3]
+
+ def __init__(self, n: int, k: int, w: int, memory_bound=inf, **kwargs):
+ if not kwargs.get("excluded_algorithms"):
+ kwargs["excluded_algorithms"] = []
+
+ kwargs["excluded_algorithms"] += self.excluded_algorithms_by_default
+
+ super(SDEstimator, self).__init__(SDAlgorithm, SDProblem(
+ n, k, w, memory_bound=memory_bound, **kwargs), **kwargs)
+
+ def table(self, show_quantum_complexity=0, show_tilde_o_time=0,
+ show_all_parameters=0, precision=1, truncate=0):
+ """
+ Print table describing the complexity of each algorithm and its optimal parameters
+
+ INPUT:
+
+ - ``show_quantum_complexity`` -- show quantum time complexity (default: true)
+ - ``show_tilde_o_time`` -- show Ε time complexity (default: true)
+ - ``show_all_parameters`` -- show all optimization parameters (default: true)
+ - ``precision`` -- number of decimal digits output (default: 1)
+ - ``truncate`` -- truncate rather than round the output (default: false)
+
+ EXAMPLES:
+
+ sage: from cryptographic_estimators.SDEstimator import SDEstimator
+ sage: A = SDEstimator(n=100, k=50, w=10)
+ sage: A.table()
+ +---------------+---------------+
+ | | estimate |
+ +---------------+------+--------+
+ | algorithm | time | memory |
+ +---------------+------+--------+
+ | BallCollision | 23.3 | 16.0 |
+ | BJMMdw | 23.4 | 14.7 |
+ | BJMMpdw | 23.3 | 14.3 |
+ | BJMM | 22.8 | 15.0 |
+ | BJMM_plus | 22.8 | 15.0 |
+ | BothMay | 22.4 | 14.7 |
+ | Dumer | 22.7 | 16.4 |
+ | MayOzerov | 22.3 | 14.8 |
+ | Prange | 28.3 | 12.7 |
+ | Stern | 22.3 | 16.0 |
+ +---------------+------+--------+
+
+ TESTS:
+
+ sage: from cryptographic_estimators.SDEstimator import SDEstimator
+ sage: A = SDEstimator(n=100, k=42, w=13, bit_complexities=1, workfactor_accuracy=25)
+ sage: A.table(show_tilde_o_time=1, precision=0) # long time
+ +---------------+---------------+------------------+
+ | | estimate | tilde_o_estimate |
+ +---------------+------+--------+-------+----------+
+ | algorithm | time | memory | time | memory |
+ +---------------+------+--------+-------+----------+
+ | BallCollision | 24 | 16 | 11 | 3 |
+ | BJMMdw | 24 | 14 | -- | -- |
+ | BJMMpdw | 24 | 15 | -- | -- |
+ | BJMM | 23 | 15 | 9 | 7 |
+ | BJMM_plus | 23 | 15 | -- | -- |
+ | BothMay | 23 | 14 | 9 | 7 |
+ | Dumer | 23 | 16 | 11 | 3 |
+ | MayOzerov | 23 | 15 | 9 | 8 |
+ | Prange | 29 | 13 | 11 | 0 |
+ | Stern | 23 | 16 | 11 | 3 |
+ +---------------+------+--------+-------+----------+
+
+ sage: from cryptographic_estimators.SDEstimator import SDEstimator
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMdw
+ sage: A = SDEstimator(3488,2720,64,excluded_algorithms=[BJMMdw])
+ sage: A.table(precision=3, show_all_parameters=1) # long time
+ +---------------+--------------------------------------------------------------------------------+
+ | | estimate |
+ +---------------+---------+---------+------------------------------------------------------------+
+ | algorithm | time | memory | parameters |
+ +---------------+---------+---------+------------------------------------------------------------+
+ | BallCollision | 151.460 | 49.814 | {'r': 7, 'p': 4, 'pl': 0, 'l': 39} |
+ | BJMMpdw | 143.448 | 86.221 | {'r': 7, 'p': 12, 'p1': 8, 'w2': 0} |
+ | BJMM | 141.886 | 104.057 | {'r': 7, 'depth': 3, 'p': 16, 'p1': 6, 'p2': 12, 'l': 197} |
+ | BJMM_plus | 142.111 | 97.541 | {'r': 7, 'p': 14, 'p1': 10, 'l': 167, 'l1': 81} |
+ | BothMay | 141.711 | 87.995 | {'r': 7, 'p': 12, 'w1': 0, 'w2': 0, 'p1': 9, 'l': 79} |
+ | Dumer | 151.380 | 58.019 | {'r': 7, 'l': 47, 'p': 5} |
+ | MayOzerov | 140.795 | 86.592 | {'r': 7, 'depth': 3, 'p': 12, 'p1': 5, 'p2': 10, 'l': 95} |
+ | Prange | 173.388 | 21.576 | {'r': 7} |
+ | Stern | 151.409 | 49.814 | {'r': 7, 'p': 4, 'l': 39} |
+ +---------------+---------+---------+------------------------------------------------------------+
+
+ sage: from cryptographic_estimators.SDEstimator import SDEstimator
+ sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMdw
+ sage: A = SDEstimator(3488,2720,64,excluded_algorithms=[BJMMdw],memory_access=3)
+ sage: A.table(precision=3, show_all_parameters=1) # long time
+ +---------------+------------------------------------------------------------------------+
+ | | estimate |
+ +---------------+---------+--------+-----------------------------------------------------+
+ | algorithm | time | memory | parameters |
+ +---------------+---------+--------+-----------------------------------------------------+
+ | BallCollision | 163.650 | 32.587 | {'r': 7, 'p': 2, 'pl': 0, 'l': 21} |
+ | BJMMpdw | 162.998 | 30.600 | {'r': 7, 'p': 2, 'p1': 1, 'w2': 0} |
+ | BJMM | 162.976 | 30.619 | {'r': 7, 'depth': 2, 'p': 2, 'p1': 1, 'l': 21} |
+ | BJMM_plus | 161.294 | 24.602 | {'r': 7, 'p': 2, 'p1': 1, 'l': 21, 'l1': 9} |
+ | BothMay | 160.317 | 25.172 | {'r': 7, 'p': 2, 'w1': 0, 'w2': 0, 'p1': 1, 'l': 8} |
+ | Dumer | 163.635 | 32.608 | {'r': 7, 'l': 21, 'p': 2} |
+ | MayOzerov | 160.311 | 25.179 | {'r': 7, 'depth': 2, 'p': 2, 'p1': 1, 'l': 8} |
+ | Prange | 180.580 | 21.576 | {'r': 7} |
+ | Stern | 163.260 | 32.587 | {'r': 7, 'p': 2, 'l': 21} |
+ +---------------+---------+--------+-----------------------------------------------------+
+
+ """
+ super(SDEstimator, self).table(show_quantum_complexity=show_quantum_complexity,
+ show_tilde_o_time=show_tilde_o_time,
+ show_all_parameters=show_all_parameters,
+ precision=precision, truncate=truncate)
diff --git a/cryptographic_estimators/SDEstimator/sd_helper.py b/cryptographic_estimators/SDEstimator/sd_helper.py
new file mode 100644
index 00000000..4f2b4664
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/sd_helper.py
@@ -0,0 +1,233 @@
+# ****************************************************************************
+# 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 math import log2, comb, inf, ceil
+
+
+def binom(n: int, k: int):
+ """
+ binomial coefficient
+ """
+ return comb(int(n), int(k))
+
+
+def min_max(a: int, b: int, s: bool):
+ """
+ Returns min(a,b) or max(a,b) depending on the switch s
+ s = false true
+ """
+ if s:
+ return max(a, b)
+ else:
+ return min(a, b)
+
+
+def __truncate(x: float, precision: int):
+ """
+ Truncates a float
+
+ INPUT:
+
+ - ``x`` -- value to be truncated
+ - ``precision`` -- number of decimal places to after which the ``x`` is truncated
+
+ """
+
+ return float(int(x * 10 ** precision) / 10 ** precision)
+
+
+def __round_or_truncate_to_given_precision(T: float, M: float, truncate: bool, precision: int):
+ """
+ rounds or truncates the inputs `T`, `M`
+ INPUT:
+ - ``T`` -- first value to truncate or round
+ - ``M`` -- second value to truncate or round
+ - ``truncate`` -- if set the `true` the inputs are truncated otherwise rounded
+ - ``precision`` -- precision of the truncation or rounding
+ """
+ if truncate:
+ T, M = __truncate(T, precision), __truncate(M, precision)
+ else:
+ T, M = round(T, precision), round(M, precision)
+ return '{:.{p}f}'.format(T, p=precision), '{:.{p}f}'.format(M, p=precision)
+
+
+def _gaussian_elimination_complexity(n: int, k: int, r: int):
+ """
+ Complexity estimate of Gaussian elimination routine
+
+ INPUT:
+
+ - ``n`` -- Row additions are perfomed on ``n`` coordinates
+ - ``k`` -- Matrix consists of ``n-k`` rows
+ - ``r`` -- Blocksize of method of the four russian for inversion, default is zero
+
+ [Bar07]_ Bard, G.V.: Algorithms for solving linear and polynomial systems of equations over finite fields
+ with applications to cryptanalysis. Ph.D. thesis (2007)
+
+ [BLP08] Bernstein, D.J., Lange, T., Peters, C.: Attacking and defending the mceliece cryptosystem.
+ In: International Workshop on Post-Quantum Cryptography. pp. 31β46. Springer (2008)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator import _gaussian_elimination_complexity
+ sage: _gaussian_elimination_complexity(n=100,k=20,r=1) # random
+
+ """
+
+ if r != 0:
+ return (r ** 2 + 2 ** r + (n - k - r)) * int(((n + r - 1) / r))
+
+ return (n - k) ** 2
+
+
+def _optimize_m4ri(n: int, k: int, mem=inf):
+ """
+ Find optimal blocksize for Gaussian elimination via M4RI
+
+ INPUT:
+
+ - ``n`` -- Row additons are perfomed on ``n`` coordinates
+ - ``k`` -- Matrix consists of ``n-k`` rows
+
+ """
+
+ (r, v) = (0, inf)
+ for i in range(n - k):
+ tmp = log2(_gaussian_elimination_complexity(n, k, i))
+ if v > tmp and r < mem:
+ r = i
+ v = tmp
+ return r
+
+
+def _mem_matrix(n: int, k: int, r: int):
+ """
+ Memory usage of parity check matrix in vector space elements
+
+ INPUT:
+
+ - ``n`` -- length of the code
+ - ``k`` -- dimension of the code
+ - ``r`` -- block size of M4RI procedure
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator import _mem_matrix
+ sage: _mem_matrix(n=100,k=20,r=0) # random
+
+ """
+ return n - k + 2 ** r
+
+
+def _list_merge_complexity(L: float, l: int, hmap: bool):
+ """
+ Complexity estimate of merging two lists exact
+
+ INPUT:
+
+ - ``L`` -- size of lists to be merged
+ - ``l`` -- amount of bits used for matching
+ - ``hmap`` -- indicates if hashmap is being used (Default 0: no hashmap)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator import _list_merge_complexity
+ sage: _list_merge_complexity(L=2**16,l=16,hmap=1) # random
+
+ """
+
+ if L == 1:
+ return 1
+ if not hmap:
+ return max(1, 2 * int(log2(L)) * L + L ** 2 // 2 ** l)
+ else:
+ return 2 * L + L ** 2 // 2 ** l
+
+
+def _indyk_motwani_complexity(L: float, l: int, w: int, hmap: bool):
+ """
+ Complexity of Indyk-Motwani nearest neighbor search
+
+ INPUT:
+
+ - ``L`` -- size of lists to be matched
+ - ``l`` -- amount of bits used for matching
+ - ``w`` -- target weight
+ - ``hmap`` -- indicates if hashmap is being used (Default 0: no hashmap)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator import _indyk_motwani_complexity
+ sage: _indyk_motwani_complexity(L=2**16,l=16,w=2,hmap=1) # random
+
+ """
+
+ if w == 0:
+ return _list_merge_complexity(L, l, hmap)
+ lam = max(0, int(min(ceil(log2(L)), l - 2 * w)))
+ return binom(l, lam) // binom(l - w, lam) * _list_merge_complexity(L, lam, hmap)
+
+
+def _mitm_nn_complexity(L: float, l: int, w: int, hmap: bool):
+ """
+ Complexity of Indyk-Motwani nearest neighbor search
+
+ INPUT:
+
+ - ``L`` -- size of lists to be matched
+ - ``l`` -- amount of bits used for matching
+ - ``w`` -- target weight
+ - ``hmap`` -- indicates if hashmap is being used (Default 0: no hashmap)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator import _indyk_motwani_complexity
+ sage: _indyk_motwani_complexity(L=2**16,l=16,w=2,hmap=1) # random
+
+ """
+ if w == 0:
+ return _list_merge_complexity(L, l, hmap)
+ L1 = L * binom(l / 2, w / 2)
+ return _list_merge_complexity(L1, l, hmap)
+
+
+def _list_merge_async_complexity(L1: float, L2: float, l: int, hmap: bool = True):
+ """
+ Complexity estimate of merging two lists exact
+ INPUT:
+ - ``L`` -- size of lists to be merged
+ - ``l`` -- amount of bits used for matching
+ - ``hmap`` -- indicates if hashmap is being used (Default 0: no hashmap)
+ EXAMPLES::
+ sage: from cryptographic_estimators.SDEstimator import _list_merge_async_complexity
+ sage: _list_merge_async_complexity(L1=2**16,L2=2**14,l=16,hmap=1) # random
+ """
+
+ if L1 == 1 and L2==1:
+ return 1
+ if L1==1:
+ return L2
+ if L2==1:
+ return L1
+ if not hmap:
+ L = max(L1, L2)
+ return max(1, 2 * int(log2(L)) * L + (L1 * L2) // 2 ** l)
+ else:
+ return L1+L2 + L1*L2 // 2 ** l
+
diff --git a/cryptographic_estimators/SDEstimator/sd_problem.py b/cryptographic_estimators/SDEstimator/sd_problem.py
new file mode 100644
index 00000000..8ef17410
--- /dev/null
+++ b/cryptographic_estimators/SDEstimator/sd_problem.py
@@ -0,0 +1,100 @@
+# ****************************************************************************
+# 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_problem import BaseProblem
+from math import comb, log2
+from .sd_constants import *
+
+
+class SDProblem(BaseProblem):
+ """
+ Construct an instance of the Syndrome Decoding Problem
+
+ INPUT:
+
+ - ``n`` -- code length
+ - ``k`` -- code dimension
+ - ``w`` -- error weight
+ - ``nsolutions`` -- number of (expected) solutions of the problem in logarithmic scale
+ - ``memory_bound`` -- maximum allowed memory to use for solving the problem
+
+ """
+
+ def __init__(self, n: int, k: int, w: int, **kwargs):
+ super().__init__(**kwargs)
+ if k > n:
+ raise ValueError("k must be smaller or equal to n")
+ if w > n - k:
+ raise ValueError("w must be smaller or equal to n-k")
+ if w <= 0 or k <= 0:
+ raise ValueError("w and k must be at least 1")
+ self.parameters[SD_CODE_LENGTH] = n
+ self.parameters[SD_CODE_DIMENSION] = k
+ self.parameters[SD_ERROR_WEIGHT] = w
+
+ self.nsolutions = kwargs.get("nsolutions", max(
+ self.expected_number_solutions(), 0))
+
+ def to_bitcomplexity_time(self, basic_operations: float):
+ """
+ Returns the bit-complexity corresponding to basic_operations field additions
+
+ INPUT:
+
+ - ``basic_operations`` -- Number of field additions (logarithmic)
+
+ """
+ n = self.parameters[SD_CODE_LENGTH]
+ q = 2
+ return log2(log2(q)) + log2(n) + basic_operations
+
+ def to_bitcomplexity_memory(self, elements_to_store: float):
+ """
+ Returns the memory bit-complexity associated to a given number of elements to store
+
+ INPUT:
+
+ - ``elements_to_store`` -- number of memory operations (logarithmic)
+
+ """
+ return self.to_bitcomplexity_time(elements_to_store)
+
+ def expected_number_solutions(self):
+ """
+ Returns the logarithm of the expected number of existing solutions to the problem
+
+ """
+ n, k, w = self.get_parameters()
+ return log2(comb(n, w)) - (n - k)
+
+ def __repr__(self):
+ """
+ """
+ n, k, w = self.get_parameters()
+ rep = "syndrome decoding problem with (n,k,w) = " \
+ + "(" + str(n) + "," + str(k) + "," + str(w) + ") over Finite Field of size 2"
+ return rep
+
+ def get_parameters(self):
+ """
+ Returns the ISD paramters n, k, w
+ """
+ n = self.parameters[SD_CODE_LENGTH]
+ k = self.parameters[SD_CODE_DIMENSION]
+ w = self.parameters[SD_ERROR_WEIGHT]
+ return n, k, w
diff --git a/cryptographic_estimators/SDFqEstimator/SDFqAlgorithms/__init__.py b/cryptographic_estimators/SDFqEstimator/SDFqAlgorithms/__init__.py
new file mode 100644
index 00000000..ad9ccc43
--- /dev/null
+++ b/cryptographic_estimators/SDFqEstimator/SDFqAlgorithms/__init__.py
@@ -0,0 +1,3 @@
+from .prange import Prange
+from .stern import Stern
+from .leebrickell import LeeBrickell
diff --git a/cryptographic_estimators/SDFqEstimator/SDFqAlgorithms/leebrickell.py b/cryptographic_estimators/SDFqEstimator/SDFqAlgorithms/leebrickell.py
new file mode 100644
index 00000000..ec5a18e0
--- /dev/null
+++ b/cryptographic_estimators/SDFqEstimator/SDFqAlgorithms/leebrickell.py
@@ -0,0 +1,104 @@
+from ...SDFqEstimator.sdfq_algorithm import SDFqAlgorithm
+from ...SDFqEstimator.sdfq_problem import SDFqProblem
+from ...SDFqEstimator.sdfq_helper import binom, log2, min_max, inf
+from ...base_algorithm import optimal_parameter
+from ..sdfq_constants import *
+from types import SimpleNamespace
+
+
+class LeeBrickell(SDFqAlgorithm):
+ def __init__(self, problem: SDFqProblem, **kwargs):
+ """
+ Construct an instance of Lee-Brickells's estimator [LB88]_
+ expected weight distribution::
+ +--------------------------------+-------------------------------+
+ | <----------+ n - k +---------> | <----------+ k +------------> |
+ | w-p | p |
+ +--------------------------------+-------------------------------+
+ INPUT:
+ - ``problem`` -- SDProblem object including all necessary parameters
+
+ TESTS::
+ sage: from cryptographic_estimators.SDFqEstimator.SDFqAlgorithms import LeeBrickell
+ sage: from cryptographic_estimators.SDFqEstimator import SDFqProblem
+ sage: LeeBrickell(SDFqProblem(n=961,k=771,w=48,q=31)).time_complexity()
+ 140.31928490910389
+
+ EXAMPLES::
+ sage: from cryptographic_estimators.SDFqEstimator.SDFqAlgorithms import LeeBrickell
+ sage: from cryptographic_estimators.SDFqEstimator import SDFqProblem
+ sage: LeeBrickell(SDFqProblem(n=100,k=50,w=10,q=5))
+ Lee-Brickell estimator for syndrome decoding problem with (n,k,w) = (100,50,10) over Finite Field of size 5
+
+ """
+ self._name = "LeeBrickell"
+ super(LeeBrickell, self).__init__(problem, **kwargs)
+ _, _, w, _ = self.problem.get_parameters()
+ self.set_parameter_ranges("p", 0, max(w // 2,1))
+ self.is_syndrome_zero = int(problem.is_syndrome_zero)
+
+ @optimal_parameter
+ def p(self):
+ """
+ Return the optimal parameter $p$ used in the algorithm optimization
+
+ EXAMPLES::
+ sage: from cryptographic_estimators.SDFqEstimator.SDFqAlgorithms import LeeBrickell
+ sage: from cryptographic_estimators.SDFqEstimator import SDFqProblem
+ sage: A = LeeBrickell(SDFqProblem(n=100,k=50,w=10,q=5))
+ sage: A.p()
+ 2
+
+ """
+ return self._get_optimal_parameter("p")
+
+ 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)
+ if par.p > w or k < par.p or n - k < w - par.p:
+ return True
+ return False
+
+ def _time_and_memory_complexity(self, parameters: dict, verbose_information=None):
+ """
+ Return time complexity of Lee-Brickell's algorithm over Fq, q > 2 for
+ given set of parameters
+ NOTE: this optimization assumes that the algorithm is executed on the generator matrix
+
+ INPUT:
+ - ``parameters`` -- dictionary including parameters
+ - ``verbose_information`` -- if set to a dictionary `permutations`,
+ `gauΓ` and `list` will be returned.
+ EXAMPLES::
+ sage: from cryptographic_estimators.SDFqEstimator.SDFqAlgorithms import LeeBrickell
+ sage: from cryptographic_estimators.SDFqEstimator import SDFqProblem
+ sage: A = LeeBrickell(SDFqProblem(n=100,k=50,q=3,w=10))
+ sage: A.p()
+ 2
+
+ """
+ n, k, w, q = self.problem.get_parameters()
+ par = SimpleNamespace(**parameters)
+
+ solutions = self.problem.nsolutions
+ enum = binom(k, par.p) * (q-1)**(max(0, par.p-self.is_syndrome_zero))
+ # Beullens code uses (contrary to his paper)
+ #enum = k**par.p * q**(max(0, par.p-self.is_syndrome_zero))
+ memory = log2(k * n)
+
+ Tp = max(log2(binom(n, w)) - log2(binom(n - k, w - par.p)) - log2(binom(k, par.p)) - solutions, 0)
+ Tg = k*k
+ time = Tp + log2(Tg + enum) + log2(n)
+ if verbose_information is not None:
+ verbose_information[VerboseInformation.PERMUTATIONS.value] = Tp
+ verbose_information[VerboseInformation.GAUSS.value] = Tg
+ verbose_information[VerboseInformation.LISTS.value] = [enum]
+
+ return time, memory
+
+ def __repr__(self):
+ rep = "Lee-Brickell estimator for " + str(self.problem)
+ return rep
diff --git a/cryptographic_estimators/SDFqEstimator/SDFqAlgorithms/prange.py b/cryptographic_estimators/SDFqEstimator/SDFqAlgorithms/prange.py
new file mode 100644
index 00000000..23a173c0
--- /dev/null
+++ b/cryptographic_estimators/SDFqEstimator/SDFqAlgorithms/prange.py
@@ -0,0 +1,57 @@
+from ...SDFqEstimator.sdfq_algorithm import SDFqAlgorithm
+from ...SDFqEstimator.sdfq_problem import SDFqProblem
+from ...SDFqEstimator.sdfq_helper import _mem_matrix, binom, log2
+from ..sdfq_constants import *
+
+
+class Prange(SDFqAlgorithm):
+ def __init__(self, problem: SDFqProblem, **kwargs):
+ """
+ Construct an instance of Prange's estimator [Pra62]_
+ expected weight distribution::
+ +--------------------------------+-------------------------------+
+ | <----------+ n - k +---------> | <----------+ k +------------> |
+ | w | 0 |
+ +--------------------------------+-------------------------------+
+
+ INPUT:
+ - ``problem`` -- SDProblem object including all necessary parameters
+
+ EXAMPLES::
+ sage: from cryptographic_estimators.SDFqEstimator.SDFqAlgorithms import Prange
+ sage: from cryptographic_estimators.SDFqEstimator import SDFqProblem
+ sage: Prange(SDFqProblem(n=100,k=50,w=10,q=3))
+ Prange estimator for syndrome decoding problem with (n,k,w) = (100,50,10) over Finite Field of size 3
+
+ """
+ self._name = "Prange"
+ super(Prange, self).__init__(problem, **kwargs)
+
+ def _time_and_memory_complexity(self, parameters: dict, verbose_information=None):
+ """
+ Return time complexity of Prange's algorithm for given set of parameters
+
+ INPUT:
+ - ``parameters`` -- dictionary including parameters
+ - ``verbose_information`` -- if set to a dictionary `permutations` and `gauΓ` will be returned.
+
+ """
+
+ n, k, w, q = self.problem.get_parameters()
+ solutions = self.problem.nsolutions
+
+ memory = log2(_mem_matrix(n, k, 0)) + log2(n)
+
+ Tp = max(log2(binom(n, w)) - log2(binom(n - k, w)) - solutions, 0)
+ Tg = log2(k*k)
+ time = Tp + Tg + log2(n)
+
+ if verbose_information is not None:
+ verbose_information[VerboseInformation.PERMUTATIONS.value] = Tp
+ verbose_information[VerboseInformation.GAUSS.value] = Tg
+
+ return time, memory
+
+ def __repr__(self):
+ rep = "Prange estimator for " + str(self.problem)
+ return rep
diff --git a/cryptographic_estimators/SDFqEstimator/SDFqAlgorithms/stern.py b/cryptographic_estimators/SDFqEstimator/SDFqAlgorithms/stern.py
new file mode 100644
index 00000000..4c52eb09
--- /dev/null
+++ b/cryptographic_estimators/SDFqEstimator/SDFqAlgorithms/stern.py
@@ -0,0 +1,154 @@
+from ...base_algorithm import optimal_parameter
+from ...SDFqEstimator.sdfq_algorithm import SDFqAlgorithm
+from ...SDFqEstimator.sdfq_problem import SDFqProblem
+from ...SDFqEstimator.sdfq_helper import _mem_matrix, binom, log2, min_max, inf
+from types import SimpleNamespace
+from ..sdfq_constants import *
+#from ..SDFqWorkfactorModels.stern import SternScipyModel
+
+
+class Stern(SDFqAlgorithm):
+ def __init__(self, problem: SDFqProblem, **kwargs):
+ """
+ Construct an instance of Stern's estimator [Pet11]_, [Ste88]_, [BLP08]_.
+
+ Expected weight distribution::
+
+ +-------------------------+---------+-------------+-------------+
+ | <----+ n - k - l +----> |<-- l -->|<--+ k/2 +-->|<--+ k/2 +-->|
+ | w - 2p | 0 | p | p |
+ +-------------------------+---------+-------------+-------------+
+
+ INPUT:
+
+ - ``problem`` -- SDProblem object including all necessary parameters
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDFqEstimator.SDFqAlgorithms import Stern
+ sage: from cryptographic_estimators.SDFqEstimator import SDFqProblem
+ sage: Stern(SDFqProblem(n=100,k=50,w=10,q=3))
+ Stern estimator for syndrome decoding problem with (n,k,w) = (100,50,10) over Finite Field of size 3
+
+ """
+ self._name = "Stern"
+ super(Stern, self).__init__(problem, **kwargs)
+ n, k, w, _ = self.problem.get_parameters()
+ self.set_parameter_ranges("p", 0, max(w // 2, 1))
+ self.set_parameter_ranges("l", 0, n-k)
+
+ @optimal_parameter
+ def l(self):
+ """
+ Return the optimal parameter $l$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDFqEstimator.SDFqAlgorithms import Stern
+ sage: from cryptographic_estimators.SDFqEstimator import SDFqProblem
+ sage: A = Stern(SDFqProblem(n=100,k=50,w=10,q=3))
+ sage: A.l()
+ 7
+
+ """
+
+ return self._get_optimal_parameter("l")
+
+ @optimal_parameter
+ def p(self):
+ """
+ Return the optimal parameter $p$ used in the algorithm optimization
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDFqEstimator.SDFqAlgorithms import Stern
+ sage: from cryptographic_estimators.SDFqEstimator import SDFqProblem
+ sage: A = Stern(SDFqProblem(n=100,k=50,w=10,q=3))
+ sage: A.p()
+ 2
+
+ """
+
+ return self._get_optimal_parameter("p")
+
+ 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 // 2
+ if par.p > w or k1 < par.p or n - k - par.l < 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()
+
+ _, k, _, q = self.problem.get_parameters()
+ k1 = k//2
+ for p in range(new_ranges["p"]["min"], min(k1, new_ranges["p"]["max"])):
+ l_val = int(log2(binom(k1, p)) - log2(q-1)*p)
+ l_search_radius = self._adjust_radius
+ for l in range(max(new_ranges["l"]["min"], l_val-l_search_radius), min(new_ranges["l"]["max"], l_val+l_search_radius)):
+ indices = {"p": p, "l": l}
+ if self._are_parameters_invalid(indices):
+ continue
+ yield indices
+
+ def _time_and_memory_complexity(self, parameters: dict, verbose_information=None):
+ """
+ Return time complexity of Sterns's algorithm over Fq for given set of
+ parameters. Code originaly from
+ - https://github.com/secomms/pkpattack/blob/main/cost_isd.sage
+ which was adapted from:
+ - https://github.com/christianepetersisdfq/blob/master/isdfq.gp
+
+ INPUT:
+ - ``parameters`` -- dictionary including parameters
+ - ``verbose_information`` -- if set to a dictionary `permutations`,
+ `gauΓ` and `list` will be returned.
+
+ """
+ n, k, w, q = self.problem.get_parameters()
+ par = SimpleNamespace(**parameters)
+ k1 = k // 2
+
+ memory_bound = self.problem.memory_bound
+
+ L1 = binom(k1, par.p) * (q-1)**par.p
+ L2 = binom(k-k1, par.p) * (q-1)**par.p
+ if self._is_early_abort_possible(log2(L1)):
+ return inf, inf
+
+ memory = log2((L1 + L2) * par.l + _mem_matrix(n, k, 0)) + log2(n)
+ solutions = self.problem.nsolutions
+
+ if memory > memory_bound:
+ return inf, inf
+
+ Tp = max(0,
+ log2(binom(n, w)) - log2(binom(n - k - par.l, w - 2 * par.p)) - log2(binom(k1, par.p)**2) - solutions)
+
+ Tg = (n-k)**2 * (n+k) // 2
+
+ build = max(((k1 - par.p + 1) + (L2+L1)) * par.l, 1)
+ cost_early_exit = max(1,int(max(q/(q-1) * (w - 2 * par.p + 1) * 2*par.p *(1 + (q - 2)/(q - 1)), 1)))
+ L = L1*L2//q**par.l
+ ops = build + L*cost_early_exit
+ time = log2(Tg + ops) + Tp
+
+ if verbose_information is not None:
+ verbose_information[VerboseInformation.PERMUTATIONS.value] = Tp
+ verbose_information[VerboseInformation.GAUSS.value] = log2(Tg)
+ verbose_information[VerboseInformation.LISTS.value] = [log2(L2), log2(L)]
+
+ return time, memory
+
+ def __repr__(self):
+ rep = "Stern estimator for " + str(self.problem)
+ return rep
diff --git a/cryptographic_estimators/SDFqEstimator/__init__.py b/cryptographic_estimators/SDFqEstimator/__init__.py
new file mode 100644
index 00000000..32a9b949
--- /dev/null
+++ b/cryptographic_estimators/SDFqEstimator/__init__.py
@@ -0,0 +1,4 @@
+from .sdfq_algorithm import SDFqAlgorithm
+from .sdfq_estimator import SDFqEstimator
+from .sdfq_problem import SDFqProblem
+from .SDFqAlgorithms import Prange, LeeBrickell, Stern
diff --git a/cryptographic_estimators/SDFqEstimator/sdfq_algorithm.py b/cryptographic_estimators/SDFqEstimator/sdfq_algorithm.py
new file mode 100644
index 00000000..e60ee546
--- /dev/null
+++ b/cryptographic_estimators/SDFqEstimator/sdfq_algorithm.py
@@ -0,0 +1,69 @@
+# ****************************************************************************
+# 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 ..helper import ComplexityType
+from ..base_algorithm import BaseAlgorithm, optimal_parameter
+from .sdfq_problem import SDFqProblem
+from .sdfq_helper import _optimize_m4ri
+from math import inf, log2
+
+
+class SDFqAlgorithm(BaseAlgorithm):
+ """
+ Base class for Syndrome Decoding over FQ algorithms complexity estimator
+
+ INPUT:
+
+ - ``problem`` -- SDFqProblem object including all necessary parameters
+ - ``hmp`` -- Indicates if Hashmap is used for list matching, if false sorting is used (default: true)
+ """
+
+ def __init__(self, problem: SDFqProblem, **kwargs):
+ super(SDFqAlgorithm, self).__init__(problem, **kwargs)
+ self._hmap = kwargs.get("hmap", 1)
+ self._adjust_radius = kwargs.get("adjust_radius", 10)
+
+ def _time_and_memory_complexity(self, parameters: dict, verbose_information=None):
+ """
+ Computes time and memory complexity for given parameters
+ """
+ raise NotImplementedError
+
+ def _compute_time_complexity(self, parameters: dict):
+ return self._time_and_memory_complexity(parameters)[0]
+
+ def _compute_memory_complexity(self, parameters: dict):
+ return self._time_and_memory_complexity(parameters)[1]
+
+ def _get_verbose_information(self):
+ """
+ returns a dictionary containing
+ {
+ CONSTRAINTS,
+ PERMUTATIONS,
+ TREE,
+ GAUSS,
+ REPRESENTATIONS,
+ LISTS
+ }
+ """
+ verb = dict()
+ _ = self._time_and_memory_complexity(self.optimal_parameters(), verbose_information=verb)
+ return verb
+
+ def __repr__(self):
+ pass
diff --git a/cryptographic_estimators/SDFqEstimator/sdfq_constants.py b/cryptographic_estimators/SDFqEstimator/sdfq_constants.py
new file mode 100644
index 00000000..fa59d208
--- /dev/null
+++ b/cryptographic_estimators/SDFqEstimator/sdfq_constants.py
@@ -0,0 +1,39 @@
+
+# ****************************************************************************
+# 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 enum import Enum
+
+SDFQ_CODE_LENGTH = "code length"
+SDFQ_CODE_DIMENSION = "code dimension"
+SDFQ_ERROR_WEIGHT = "error weight"
+SDFQ_ERROR_FIELD_SIZE = "field size"
+
+class VerboseInformation(Enum):
+ """
+ """
+ CONSTRAINTS = "constraints"
+ PERMUTATIONS = "permutations"
+ TREE = "tree"
+ GAUSS = "gauss"
+ REPRESENTATIONS = "representation"
+ LISTS = "lists"
diff --git a/cryptographic_estimators/SDFqEstimator/sdfq_estimator.py b/cryptographic_estimators/SDFqEstimator/sdfq_estimator.py
new file mode 100644
index 00000000..f967954a
--- /dev/null
+++ b/cryptographic_estimators/SDFqEstimator/sdfq_estimator.py
@@ -0,0 +1,76 @@
+from ..SDFqEstimator.sdfq_algorithm import SDFqAlgorithm
+from ..SDFqEstimator.sdfq_problem import SDFqProblem
+from ..base_estimator import BaseEstimator
+from math import inf
+
+
+class SDFqEstimator(BaseEstimator):
+ """
+
+ INPUT:
+
+ - ``n`` -- code length
+ - ``k`` -- code dimension
+ - ``w`` -- error weight
+ - ``q`` -- base field size
+ - ``excluded_algorithms`` -- a list/tuple of excluded algorithms (default: None)
+ - ``nsolutions`` -- no. of solutions
+
+ """
+ excluded_algorithms_by_default = []
+
+ def __init__(self, n: int, k: int, w: int, q: int, memory_bound=inf, **kwargs):
+ if not kwargs.get("excluded_algorithms"):
+ kwargs["excluded_algorithms"] = []
+
+ kwargs["excluded_algorithms"] += self.excluded_algorithms_by_default
+ super(SDFqEstimator, self).__init__(SDFqAlgorithm, SDFqProblem(
+ n, k, w, q, memory_bound=memory_bound, **kwargs), **kwargs)
+
+ def table(self, show_quantum_complexity=0, show_tilde_o_time=0,
+ show_all_parameters=0, precision=1, truncate=0):
+ """
+ Print table describing the complexity of each algorithm and its optimal parameters
+
+ INPUT:
+
+ - ``show_quantum_complexity`` -- show quantum time complexity (default: true)
+ - ``show_tilde_o_time`` -- show Ε time complexity (default: true)
+ - ``show_all_parameters`` -- show all optimization parameters (default: true)
+ - ``precision`` -- number of decimal digits output (default: 1)
+ - ``truncate`` -- truncate rather than round the output (default: false)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDFqEstimator import SDFqEstimator
+ sage: A = SDFqEstimator(n=100,k=50,w=10,q=5)
+ sage: A.table()
+ +-------------+---------------+
+ | | estimate |
+ +-------------+------+--------+
+ | algorithm | time | memory |
+ +-------------+------+--------+
+ | Prange | 29.9 | 13.5 |
+ | Stern | 24.3 | 23.9 |
+ | LeeBrickell | 25.4 | 13.5 |
+ +-------------+------+--------+
+
+ TESTS::
+
+ sage: from cryptographic_estimators.SDFqEstimator import SDFqEstimator
+ sage: A = SDFqEstimator(961,771,48,31)
+ sage: A.table(precision=3, show_all_parameters=1) # long time
+ +-------------+-------------------------------------+
+ | | estimate |
+ +-------------+---------+--------+------------------+
+ | algorithm | time | memory | parameters |
+ +-------------+---------+--------+------------------+
+ | Prange | 151.310 | 19.794 | {} |
+ | Stern | 129.059 | 42.016 | {'p': 2, 'l': 7} |
+ | LeeBrickell | 140.319 | 21.808 | {'p': 2} |
+ +-------------+---------+--------+------------------+
+ """
+ super(SDFqEstimator, self).table(show_quantum_complexity=show_quantum_complexity,
+ show_tilde_o_time=show_tilde_o_time,
+ show_all_parameters=show_all_parameters,
+ precision=precision, truncate=truncate)
diff --git a/cryptographic_estimators/SDFqEstimator/sdfq_helper.py b/cryptographic_estimators/SDFqEstimator/sdfq_helper.py
new file mode 100644
index 00000000..5b0382d7
--- /dev/null
+++ b/cryptographic_estimators/SDFqEstimator/sdfq_helper.py
@@ -0,0 +1,165 @@
+# ****************************************************************************
+# 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 math import log2, comb, inf, ceil
+
+
+def binom(n: int, k: int):
+ """
+ binomial coefficient
+ """
+ return comb(int(n), int(k))
+
+
+def min_max(a: int, b: int, s: bool):
+ """
+ Returns min(a,b) or max(a,b) depending on the switch s
+ s = false true
+ """
+ if s:
+ return max(a, b)
+ else:
+ return min(a, b)
+
+
+def __truncate(x: float, precision: int):
+ """
+ Truncates a float
+
+ INPUT:
+
+ - ``x`` -- value to be truncated
+ - ``precision`` -- number of decimal places to after which the ``x`` is truncated
+
+ """
+
+ return float(int(x * 10 ** precision) / 10 ** precision)
+
+
+def __round_or_truncate_to_given_precision(T: float, M: float, truncate: bool, precision: int):
+ """
+ rounds or truncates the inputs `T`, `M`
+ INPUT:
+ - ``T`` -- first value to truncate or round
+ - ``M`` -- second value to truncate or round
+ - ``truncate`` -- if set the `true` the inputs are truncated otherwise rounded
+ - ``precision`` -- precision of the truncation or rounding
+ """
+ if truncate:
+ T, M = __truncate(T, precision), __truncate(M, precision)
+ else:
+ T, M = round(T, precision), round(M, precision)
+ return '{:.{p}f}'.format(T, p=precision), '{:.{p}f}'.format(M, p=precision)
+
+
+def _gaussian_elimination_complexity(n: int, k: int, r: int):
+ """
+ Complexity estimate of Gaussian elimination routine
+
+ INPUT:
+
+ - ``n`` -- Row additions are perfomed on ``n`` coordinates
+ - ``k`` -- Matrix consists of ``n-k`` rows
+ - ``r`` -- Blocksize of method of the four russian for inversion, default is zero
+
+ [Bar07]_ Bard, G.V.: Algorithms for solving linear and polynomial systems of equations over finite fields
+ with applications to cryptanalysis. Ph.D. thesis (2007)
+
+ [BLP08] Bernstein, D.J., Lange, T., Peters, C.: Attacking and defending the mceliece cryptosystem.
+ In: International Workshop on Post-Quantum Cryptography. pp. 31β46. Springer (2008)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator import _gaussian_elimination_complexity
+ sage: _gaussian_elimination_complexity(n=100,k=20,r=1) # random
+
+ """
+
+ if r != 0:
+ return (r ** 2 + 2 ** r + (n - k - r)) * int(((n + r - 1) / r))
+
+ return (n - k) ** 2
+
+
+def _optimize_m4ri(n: int, k: int, mem=inf):
+ """
+ Find optimal blocksize for Gaussian elimination via M4RI
+
+ INPUT:
+
+ - ``n`` -- Row additons are perfomed on ``n`` coordinates
+ - ``k`` -- Matrix consists of ``n-k`` rows
+
+ """
+
+ (r, v) = (0, inf)
+ for i in range(n - k):
+ tmp = log2(_gaussian_elimination_complexity(n, k, i))
+ if v > tmp and r < mem:
+ r = i
+ v = tmp
+ return r
+
+
+def _mem_matrix(n: int, k: int, r: int):
+ """
+ Memory usage of parity check matrix in vector space elements
+
+ INPUT:
+
+ - ``n`` -- length of the code
+ - ``k`` -- dimension of the code
+ - ``r`` -- block size of M4RI procedure
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator import _mem_matrix
+ sage: _mem_matrix(n=100,k=20,r=0) # random
+
+ """
+ return n - k + 2 ** r
+
+
+def _list_merge_complexity(L: float, l: int, hmap: bool):
+ """
+ TODO remove?
+ Complexity estimate of merging two lists exact
+
+ INPUT:
+
+ - ``L`` -- size of lists to be merged
+ - ``l`` -- amount of bits used for matching
+ - ``hmap`` -- indicates if hashmap is being used (Default 0: no hashmap)
+
+ EXAMPLES::
+
+ sage: from cryptographic_estimators.SDEstimator import _list_merge_complexity
+ sage: _list_merge_complexity(L=2**16,l=16,hmap=1) # random
+
+ """
+
+ if L == 1:
+ return 1
+ if not hmap:
+ return max(1, 2 * int(log2(L)) * L + L ** 2 // 2 ** l)
+ else:
+ return 2 * L + L ** 2 // 2 ** l
diff --git a/cryptographic_estimators/SDFqEstimator/sdfq_problem.py b/cryptographic_estimators/SDFqEstimator/sdfq_problem.py
new file mode 100644
index 00000000..b3e0e31c
--- /dev/null
+++ b/cryptographic_estimators/SDFqEstimator/sdfq_problem.py
@@ -0,0 +1,104 @@
+# ****************************************************************************
+# 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_problem import BaseProblem
+from math import log2, comb
+from .sdfq_constants import *
+
+
+class SDFqProblem(BaseProblem):
+ """
+ Construct an instance of the Syndrome Decoding over Fq Problem
+
+ INPUT:
+
+ - ``n`` -- code length
+ - ``k`` -- code dimension
+ - ``w`` -- error weight
+ - ``q`` -- size of the basefield of the code
+ - ``nsolutions`` -- number of (expected) solutions of the problem in logarithmic scale
+ - ``is_syndrome_zero`` -- if set to true, special algorithmic optimizations can be applied (default: True)
+ """
+
+ def __init__(self, n: int, k: int, w: int, q: int, **kwargs): # Fill with parameters
+ super().__init__(**kwargs)
+ if k > n:
+ raise ValueError("k must be smaller or equal to n")
+ if w > n - k:
+ raise ValueError("w must be smaller or equal to n-k")
+ if w <= 0 or k <= 0:
+ raise ValueError("w and k must be at least 1")
+ if q <= 2:
+ raise ValueError("q must be at least 3")
+ self.parameters[SDFQ_CODE_LENGTH] = n
+ self.parameters[SDFQ_CODE_DIMENSION] = k
+ self.parameters[SDFQ_ERROR_WEIGHT] = w
+ self.parameters[SDFQ_ERROR_FIELD_SIZE] = q
+
+ self.nsolutions = kwargs.get("nsolutions", max(self.expected_number_solutions(), 0))
+ self.is_syndrome_zero = kwargs.get("is_syndrome_zero", True)
+
+ def to_bitcomplexity_time(self, basic_operations:float):
+ """
+ Returns the bit-complexity corresponding to basic_operations field additions
+
+ INPUT:
+
+ - ``basic_operations`` -- Number of field additions (logarithmic)
+
+ """
+ _,_,_,q=self.get_parameters()
+ return basic_operations + log2(log2(q))
+
+ def to_bitcomplexity_memory(self, elements_to_store: float):
+ """
+ Returns the memory bit-complexity associated to a given number of elements to store
+
+ INPUT:
+
+ - ``elements_to_store`` -- number of elements to store (logarithmic)
+
+ """
+ return self.to_bitcomplexity_time(elements_to_store)
+
+ def expected_number_solutions(self):
+ """
+ Returns the logarithm of the expected number of existing solutions to the problem
+
+ """
+ n, k, w, q = self.get_parameters()
+ Nw = log2(comb(n, w)) + log2(q-1)*(w-2) + log2(q)*(k + 1 - n)
+ return max(Nw, 0)
+
+ def __repr__(self):
+ """
+ """
+ n, k, w, q = self.get_parameters()
+ rep = "syndrome decoding problem with (n,k,w) = " \
+ + "(" + str(n) + "," + str(k) + "," + str(w) + ") over Finite Field of size " + str(q)
+
+ return rep
+
+ def get_parameters(self):
+ """
+ Returns the ISD paramters n, k, w, q
+ """
+ n = self.parameters[SDFQ_CODE_LENGTH]
+ k = self.parameters[SDFQ_CODE_DIMENSION]
+ w = self.parameters[SDFQ_ERROR_WEIGHT]
+ q = self.parameters[SDFQ_ERROR_FIELD_SIZE]
+ return n, k, w, q
diff --git a/cryptographic_estimators/__init__.py b/cryptographic_estimators/__init__.py
new file mode 100644
index 00000000..c4d33763
--- /dev/null
+++ b/cryptographic_estimators/__init__.py
@@ -0,0 +1,8 @@
+from .base_algorithm import BaseAlgorithm
+from .base_estimator import BaseEstimator
+from .base_problem import BaseProblem
+from .helper import ComplexityType, concat_pretty_tables, _truncate, round_or_truncate
+from . import SDEstimator
+from . import SDFqEstimator
+from . import MQEstimator
+from . import DummyEstimator
diff --git a/cryptographic_estimators/base_algorithm.py b/cryptographic_estimators/base_algorithm.py
new file mode 100644
index 00000000..6cafe9ef
--- /dev/null
+++ b/cryptographic_estimators/base_algorithm.py
@@ -0,0 +1,555 @@
+# ****************************************************************************
+# 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 typing import Union, Callable
+from .helper import ComplexityType
+from .base_problem import BaseProblem
+import functools
+from math import inf, log2
+from .base_constants import BASE_BIT_COMPLEXITIES, BASE_COMPLEXITY_TYPE, BASE_ESTIMATE, BASE_MEMORY_ACCESS, BASE_TILDEO
+
+
+class BaseAlgorithm:
+ def __init__(self, problem, **kwargs):
+ """
+ Base class for algorithms complexity estimator
+
+ INPUT:
+
+ - ``problem`` -- BaseProblem object including all necessary parameters
+ - ``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)
+ - ``complexity_type`` -- complexity type to consider (0: estimate, 1: tilde O complexity, default 0)
+ - ``bit_complexities`` -- deterimines if complexity is given in bit operations or basic operations (default 1: in bit)
+
+ """
+ self.bit_complexities = kwargs.get(BASE_BIT_COMPLEXITIES, 1)
+ self._complexity_type = kwargs.get(
+ BASE_COMPLEXITY_TYPE, ComplexityType.ESTIMATE.value)
+ self._memory_access = kwargs.get(BASE_MEMORY_ACCESS, 0)
+
+ self._optimal_parameters = dict()
+ self._verbose_information = dict()
+ self.problem = problem
+ self._time_complexity = None
+ self._memory_complexity = None
+ self._parameter_ranges = dict()
+ self._optimal_parameters_methods = self._get_optimal_parameter_methods_()
+ self._current_minimum_for_early_abort = inf
+ for i in self._optimal_parameters_methods:
+ self._parameter_ranges[i.__name__] = {}
+
+ @property
+ def parameter_ranges(self):
+ """
+ Returns the set ranges in which for optimal parameters are searched by the optimization algorithm (used only for complexity type estimate)
+ """
+ if self.complexity_type == ComplexityType.ESTIMATE.value:
+ return self._parameter_ranges
+
+ @property
+ def memory_access(self):
+ """
+ Returns the attribute _memory_access
+
+ """
+ return self._memory_access
+
+ @memory_access.setter
+ def memory_access(self, new_memory_access: Union[int, Callable[[float], float]]):
+ """
+ Sets the attribute _memory_access and resets internal state respectively
+
+ INPUT:
+
+ - ``new_memory_access`` -- new memory_access value
+
+ """
+ if new_memory_access not in [0, 1, 2, 3] and not callable(self.memory_access):
+ raise ValueError("invalid value for memory_access")
+ if self._memory_access != new_memory_access:
+ self.reset()
+ self._memory_access = new_memory_access
+
+ @property
+ def complexity_type(self):
+ """
+ Returns the attribute _complexity_type
+
+ """
+ return self._complexity_type
+
+ @complexity_type.setter
+ def complexity_type(self, input_type: Union[int, str]):
+ """
+ Sets the attribute _complexity_type and resets internal state respectively
+
+ INPUT:
+
+ - ``input_type`` -- new complexity_type value
+
+ """
+ if type(input_type) is str:
+ if input_type == BASE_ESTIMATE:
+ new_type = ComplexityType.ESTIMATE.value
+ elif input_type == BASE_TILDEO:
+ new_type = ComplexityType.TILDEO.value
+ else:
+ raise ValueError(
+ f"the complexity type should be either the string ESTIMATE or TILDEO")
+
+ elif input_type not in [ComplexityType.ESTIMATE.value, ComplexityType.TILDEO.value]:
+ raise ValueError("invalid value for complexity_type")
+
+ else:
+ new_type = input_type
+
+ if self._complexity_type != new_type:
+ self.reset()
+ self._complexity_type = new_type
+
+ def memory_access_cost(self, mem: float):
+ """
+ INPUT:
+
+ - ```mem`` -- memory consumption of an algorithm
+ - ```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)
+
+ """
+ if self._memory_access == 0:
+ return 0
+ elif self._memory_access == 1:
+ return log2(mem)
+ elif self._memory_access == 2:
+ return mem / 2
+ elif self._memory_access == 3:
+ return mem / 3
+ elif callable(self._memory_access):
+ return self._memory_access(mem)
+ return 0
+
+ def _get_verbose_information(self):
+ """
+ Returns dictionary with any additional information relevant to this algorithm
+
+ """
+ return dict()
+
+ def reset(self):
+ """
+ Resets internal state of the algorithm
+
+ """
+ self._complexity_type = ComplexityType.ESTIMATE.value
+ self._optimal_parameters = {}
+ self._time_complexity = None
+ self._memory_complexity = None
+ self._verbose_information = None
+
+ def set_parameter_ranges(self, parameter: str, min_value: float, max_value: float):
+ """
+ Set range of specific parameter (if optimal parameter is already set, it must fall in that range)
+
+ INPUT:
+
+ - ``parameter`` -- name of parameter to set
+ - ``min_value`` -- lowerbound for parameter (inclusive)
+ - ``max_value`` -- upperbound for parameter (inclusive)
+
+ """
+ if parameter not in self.parameter_names():
+ raise IndexError(
+ parameter + " is no valid parameter for " + str(self))
+ if min_value > max_value:
+ raise ValueError("minValue must be smaller or equal to maxValue")
+ if parameter in self._optimal_parameters:
+ if not (min_value <= self._optimal_parameters[parameter] <= max_value):
+ raise ValueError("current optimal parameter does not fall in this range,"
+ " reset optimal parameters or choose a different range")
+
+ self._parameter_ranges[parameter]["min"] = min_value
+ self._parameter_ranges[parameter]["max"] = max_value
+
+ def _do_valid_parameters_in_current_ranges_exist(self):
+ if any(i not in self._optimal_parameters.keys() for i in self.parameter_names()):
+ return False
+ return True
+
+ def _compute_time_complexity(self, parameters: dict):
+ """
+ Compute and return the time complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ """
+ raise NotImplementedError
+
+ def _compute_memory_complexity(self, parameters: dict):
+ """
+ Compute and return the memory complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ """
+ raise NotImplementedError
+
+ def _compute_tilde_o_time_complexity(self, parameters):
+ """
+ Compute and return the tilde-O time complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ """
+ raise NotImplementedError
+
+ def _compute_tilde_o_memory_complexity(self, parameters):
+ """
+ Compute and return the tilde-O memory complexity of the algorithm for a given set of parameters
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including the parameters
+
+ """
+ raise NotImplementedError
+
+ def _find_optimal_tilde_o_parameters(self):
+ raise NotImplementedError
+
+ def _get_optimal_parameter_methods_(self):
+ """
+ Return a list of methods decorated with @optimal_parameter ordered by linenumber of appearance
+
+ """
+ def sort_operator(v):
+ return v[1]
+
+ import inspect
+
+ def is_optimal_parameter_method(object):
+ return inspect.ismethod(object) and hasattr(object, "__wrapped__")
+
+ members = []
+ for _, f in inspect.getmembers(self, predicate=is_optimal_parameter_method):
+ _, start_line = inspect.getsourcelines(f)
+ members.append([f, start_line])
+ members.sort(key=sort_operator)
+
+ return [f[0] for f in members]
+
+ def _is_early_abort_possible(self, time_lower_bound: float):
+ """
+ checks whether the current time lower bound is below the
+ early exit limit
+ """
+ if time_lower_bound > self._current_minimum_for_early_abort:
+ return True
+ return False
+
+ def _find_optimal_parameters(self):
+ """
+ Enumerates all valid parameter configurations within the _parameter_ranges and saves the best
+ result (according to time complexity) in `_optimal_parameters`
+
+ """
+
+ time = inf
+ for params in self._valid_choices():
+ tmp_time = self._compute_time_complexity(params)
+ tmp_memory = self._compute_memory_complexity(params)
+ if self.bit_complexities:
+ tmp_memory = self.problem.to_bitcomplexity_memory(tmp_memory)
+
+ tmp_time += self.memory_access_cost(tmp_memory)
+
+ if tmp_time < time and tmp_memory <= self.problem.memory_bound:
+ time, memory = tmp_time, tmp_memory
+ self._current_minimum_for_early_abort = tmp_time
+ for i in params:
+ self._optimal_parameters[i] = params[i]
+ self._current_minimum_for_early_abort = inf
+
+ def _get_optimal_parameter(self, key: str):
+ """
+ Returns the optimal value for the parameter `key`. Either calculates the asymptotic optimization or
+ for real instances. This function is meant for fetching optimization parameters which need to be optimized together.
+
+ """
+ if key not in self._optimal_parameters:
+ if self.complexity_type == ComplexityType.ESTIMATE.value:
+ self._call_all_preceeding_optimal_parameter_functions(key)
+ self._find_optimal_parameters()
+ elif self.complexity_type == ComplexityType.TILDEO.value:
+ self._find_optimal_tilde_o_parameters()
+
+ return self._optimal_parameters.get(key)
+
+ def get_optimal_parameters_dict(self):
+ """
+ Returns the optimal parameters dictionary
+
+ """
+ return self._optimal_parameters
+
+ def _fix_ranges_for_already_set_parameters(self):
+ """
+ Returns a new parameter rangers dictionary, which fixes already
+ optimal paramters.
+ """
+ parameters = self._optimal_parameters
+ ranges = self._parameter_ranges
+ new_ranges = {i: ranges[i].copy() if i not in parameters else {"min": parameters[i], "max": parameters[i]}
+ for i in ranges}
+ return new_ranges
+
+ def _are_parameters_invalid(self, parameters: dict):
+ """
+ Specifies constraints on the parameters
+ """
+ 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()
+ indices = {i: new_ranges[i]["min"] for i in new_ranges}
+ keys = [i for i in indices]
+ stop = False
+ while not stop:
+ if not self._are_parameters_invalid(indices):
+ yield indices
+ indices[next(iter(indices))] += 1
+ for i in range(len(keys)):
+ if indices[keys[i]] > new_ranges[keys[i]]["max"]:
+ indices[keys[i]] = new_ranges[keys[i]]["min"]
+ if i != len(keys) - 1:
+ indices[keys[i + 1]] += 1
+ else:
+ stop = True
+ else:
+ break
+
+ def __set_dict(self, **kwargs):
+ """
+ Returns a dictionary of parameters whose values are all either optimized or they are all specified in kwargs.
+ """
+ params = dict()
+ if kwargs != {}:
+ missing_parameters = [
+ x for x in self.parameter_names() if x not in list(kwargs.keys())]
+ if missing_parameters:
+ raise ValueError(
+ f"values for the parameters in the list {missing_parameters} must be provided")
+ else:
+ for i in self.parameter_names():
+ params[i] = kwargs.get(i)
+ else:
+ params = self.optimal_parameters()
+ return params
+
+ def set_parameters(self, parameters: dict):
+ """
+ Set optimal parameters to predifined values:
+
+ INPUT:
+
+ - ``parameters`` -- dictionary including parameters to set (for a subset of optimal_parameters functions)
+
+ """
+ save_complexity_type = self.complexity_type
+ self.reset()
+ self.complexity_type = save_complexity_type
+
+ s = self._optimal_parameters_methods
+ for i in parameters.keys():
+ if str(i) not in [j.__name__ for j in s]:
+ raise ValueError(
+ i + " is not a valid parameter for " + str(self))
+ self._parameter_ranges[i]["max"] = max(
+ parameters[i], self._parameter_ranges[i]["max"])
+ self._parameter_ranges[i]["min"] = min(
+ parameters[i], self._parameter_ranges[i]["min"])
+
+ self._optimal_parameters[i] = parameters[i]
+
+ self._memory_complexity = None
+ self._time_complexity = None
+
+ def time_complexity(self, **kwargs):
+ """
+ Return the time complexity of the algorithm
+
+ INPUT:
+
+ - ``optimal_parameters`` -- if for each optimal parameter of the algorithm a value is provided the computation is done based on those parameters
+
+ """
+ if kwargs == {}:
+ if self._time_complexity is not None:
+ return self._time_complexity
+ else:
+ params = self.optimal_parameters()
+ if not self._do_valid_parameters_in_current_ranges_exist():
+ self._time_complexity = inf
+ self._memory_complexity = inf
+ return inf
+ else:
+ params = self.__set_dict(**kwargs)
+
+ if self._complexity_type == ComplexityType.ESTIMATE.value:
+ temp_time_complexity = self._compute_time_complexity(params)
+ if self.bit_complexities:
+ temp_time_complexity = self.problem.to_bitcomplexity_time(
+ temp_time_complexity)
+
+ if self._memory_access != 0:
+ temp_time_complexity += self.memory_access_cost(
+ self.memory_complexity())
+ else:
+ temp_time_complexity = self._compute_tilde_o_time_complexity(
+ params)
+
+ if kwargs == {}:
+ self._time_complexity = temp_time_complexity
+ return temp_time_complexity
+
+ def memory_complexity(self, **kwargs):
+ """
+ Return the memory complexity of the algorithm
+
+ INPUT:
+
+ - ``optimal_parameters`` -- if for each optimal parameter of the algorithm a value is provided the computation is done based on those parameters
+
+ """
+
+ if kwargs == {}:
+ if self._memory_complexity is not None:
+ return self._memory_complexity
+ else:
+ params = self.optimal_parameters()
+ if not self._do_valid_parameters_in_current_ranges_exist():
+ self._time_complexity = inf
+ self._memory_complexity = inf
+ return inf
+
+ else:
+ params = self.__set_dict(**kwargs)
+
+ if self._complexity_type == ComplexityType.ESTIMATE.value:
+ temp_memory_complexity = self._compute_memory_complexity(params)
+ if self.bit_complexities:
+ temp_memory_complexity = self.problem.to_bitcomplexity_memory(
+ temp_memory_complexity)
+ else:
+ temp_memory_complexity = self._compute_tilde_o_memory_complexity(
+ params)
+ if kwargs == {}:
+ self._memory_complexity = temp_memory_complexity
+ return temp_memory_complexity
+
+ def optimal_parameters(self):
+ """
+ Return a dictionary of optimal parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators import BaseAlgorithm, BaseProblem
+ sage: BaseAlgorithm(BaseProblem()).optimal_parameters()
+ {}
+ """
+ if self.has_optimal_parameter():
+ for f in self._optimal_parameters_methods:
+ _ = f()
+ return self._optimal_parameters
+
+ def _call_all_preceeding_optimal_parameter_functions(self, key: str):
+ """
+ call the decorator function for each parameter, if they are optimal.
+
+ """
+
+ if self.has_optimal_parameter():
+ for f in self._optimal_parameters_methods:
+ if f.__name__ == key:
+ break
+ _ = f()
+
+ def has_optimal_parameter(self):
+ """
+ Return `True` if the algorithm has optimal parameter
+
+ TESTS::
+
+ sage: from cryptographic_estimators import BaseAlgorithm, BaseProblem
+ sage: BaseAlgorithm(BaseProblem()).has_optimal_parameter()
+ False
+ """
+ return len(self._optimal_parameters_methods) > 0
+
+ def parameter_names(self):
+ """
+ Return the list with the names of the algorithm's parameters
+
+ TESTS::
+
+ sage: from cryptographic_estimators import BaseAlgorithm, BaseProblem
+ sage: BaseAlgorithm(BaseProblem()).parameter_names()
+ []
+ """
+ parameter_method_names = []
+ if self.has_optimal_parameter():
+ parameter_method_names = [
+ i.__name__ for i in self._optimal_parameters_methods]
+ return parameter_method_names
+
+
+def optimal_parameter(func):
+ """
+ Decorator to indicate optimization parameter in BaseAlgorithm
+
+ INPUT:
+
+ - ``func`` -- a method of a BaseAlgoritm subclass
+ """
+
+ @functools.wraps(func)
+ def optimal_parameter(*args, **kwargs):
+ name = func.__name__
+ self = args[0]
+ if name not in self._optimal_parameters:
+ temp = func(*args, **kwargs)
+ if temp is not None:
+ self._optimal_parameters[name] = temp
+ return self._optimal_parameters.get(name)
+
+ return optimal_parameter
diff --git a/cryptographic_estimators/base_constants.py b/cryptographic_estimators/base_constants.py
new file mode 100644
index 00000000..e9ebaa2f
--- /dev/null
+++ b/cryptographic_estimators/base_constants.py
@@ -0,0 +1,37 @@
+# ****************************************************************************
+# 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 .
+# ****************************************************************************
+
+
+BASE_BIT_COMPLEXITIES = "bit_complexities"
+BASE_COMPLEXITY_TYPE = "complexity_type"
+BASE_MEMORY_ACCESS = "memory_access"
+BASE_MEMORY_BOUND = "memory_bound"
+BASE_ESTIMATE = "ESTIMATE"
+BASE_TILDEO = "TILDEO"
+
+BASE_EXCLUDED_ALGORITHMS = "excluded_algorithms"
+
+BASE_ESTIMATEO = "estimate"
+BASE_TILDEO_ESTIMATE = "tilde_o_estimate"
+BASE_QUANTUMO = "quantum_estimate"
+BASE_ADDITIONALO = "additional_information"
+
+BASE_TIME = "time"
+BASE_MEMORY = "memory"
+BASE_PARAMETERS = "parameters"
+BASE_ALGORITHM = "algorithm"
+BASE_NSOLUTIONS = "nsolutions"
diff --git a/cryptographic_estimators/base_estimator.py b/cryptographic_estimators/base_estimator.py
new file mode 100644
index 00000000..77eb02b8
--- /dev/null
+++ b/cryptographic_estimators/base_estimator.py
@@ -0,0 +1,313 @@
+# ****************************************************************************
+# 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 math import isinf
+from typing import Union, Callable
+from .helper import ComplexityType
+from .base_constants import BASE_TILDEO_ESTIMATE, BASE_ADDITIONALO, BASE_BIT_COMPLEXITIES, BASE_ESTIMATEO, BASE_EXCLUDED_ALGORITHMS, BASE_MEMORY, BASE_PARAMETERS, BASE_QUANTUMO, BASE_TIME
+from .base_algorithm import BaseAlgorithm
+from .estimation_renderer import EstimationRenderer
+
+
+class BaseEstimator(object):
+ """
+ Construct an instance of BaseEstimator
+
+ INPUT:
+
+ - ``alg`` -- specialized algorithm class (subclass of BaseAlgorithm)
+ - ``prob`` -- object of any subclass of BaseProblem
+ - ``excluded_algorithms`` -- a list/tuple of excluded algorithms (default: None)
+ - ``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)
+ - ``complexity_type`` -- complexity type to consider (0: estimate, 1: tilde O complexity, default 0)
+ - ``bit_complexities`` -- state complexity as bit rather than field operations (default 1, only relevant for complexity_type 0)
+ - ``include_tildeo`` -- specifies if tildeO estimation should be included in the outputs (default 0: no tildeO esimation)
+ - ``include_quantum`` -- specifies if quantum estimation should be included in the outputs (default 0: no quyantum esimation)
+
+ """
+ excluded_algorithms_by_default = []
+
+ def __init__(self, alg, prob, **kwargs):
+
+ excluded_algorithms = kwargs.get(BASE_EXCLUDED_ALGORITHMS, tuple())
+ if excluded_algorithms:
+ if any(not issubclass(Algorithm, alg) for Algorithm in excluded_algorithms):
+ raise TypeError(
+ f"all excluded algorithms must be a subclass of {alg.__name__}")
+ del kwargs[BASE_EXCLUDED_ALGORITHMS]
+
+ self._algorithms = []
+ self.estimates = {}
+
+ self.problem = prob
+ self._bit_complexities = kwargs.get(BASE_BIT_COMPLEXITIES, 1)
+ self.bit_complexities = self._bit_complexities
+ self.include_tildeo = kwargs.get("include_tildeo", False)
+ self.include_quantum = kwargs.get("include_quantum", False)
+
+ included_algorithms = (Algorithm for Algorithm in alg.__subclasses__(
+ ) if Algorithm not in excluded_algorithms)
+
+ for Algorithm in included_algorithms:
+ try:
+ algorithm = Algorithm(prob, **kwargs)
+ except (ValueError, TypeError):
+ continue
+
+ self._algorithms.append(algorithm)
+
+ setattr(self, algorithm.__module__.split('.')[-1], algorithm)
+
+ @property
+ def memory_access(self):
+ """
+ Returns a list of memory_access attributes of included algorithms
+
+ """
+ return [i.memory_access for i in self._algorithms]
+
+ @memory_access.setter
+ def memory_access(self, new_memory_access: Union[int, Callable[[float], float]]):
+ """
+ Sets the memory_access attribute of all included algorithms
+
+ INPUT:
+
+ - ``new_memory_access`` -- new memory access value. Either (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)
+
+ """
+ for i in self._algorithms:
+ i.memory_access = new_memory_access
+
+ @property
+ def complexity_type(self):
+ """
+ Returns a list of complexity_type attributes of included algorithms
+
+ """
+ return [i.complexity_type for i in self._algorithms]
+
+ @complexity_type.setter
+ def complexity_type(self, new_complexity_type: ComplexityType):
+ """
+ Sets the complexity_type attribute of all included algorithms
+
+ INPUT:
+
+ - ``new_complexity_type`` -- new complexy_type value. Either (0: estimate, 1: tilde O complexity)
+
+ """
+ for i in self._algorithms:
+ i.complexity_type = new_complexity_type
+
+ @property
+ def bit_complexities(self):
+ """
+ Returns a list of bit_complexities attributes of included algorithms
+
+ """
+ return [i.bit_complexities for i in self._algorithms]
+
+ @bit_complexities.setter
+ def bit_complexities(self, new_bit_complexities: int):
+ """
+ Sets the bit_complexities attribute of all included algorithms
+
+ INPUT:
+
+ - ``new_bit_complexities`` -- new bit_complexities value.
+
+ """
+ if self._bit_complexities != new_bit_complexities:
+ self._bit_complexities = new_bit_complexities
+ self.reset()
+ for i in self._algorithms:
+ i.bit_complexities = new_bit_complexities
+
+ def algorithms(self):
+ """
+ Return a list of considered algorithms
+
+ """
+ return self._algorithms
+
+ def algorithm_names(self):
+ """
+ Return a list of the name of considered algorithms
+
+ """
+ return [algorithm.__class__.__name__ for algorithm in self.algorithms()]
+
+ def nalgorithms(self):
+ """
+ Return the number of considered algorithms
+
+ """
+ return len(self.algorithms())
+
+ def _add_tilde_o_complexity(self, algorithm: BaseAlgorithm):
+ """
+ runs the tilde O complexity analysis for the given `algorithm`
+
+ INPUT:
+
+ - ``algorithm`` -- Algorithm to run.
+
+ """
+ est = self.estimates
+ name = algorithm.__class__.__name__
+ algorithm.complexity_type = ComplexityType.TILDEO.value
+ est[name][BASE_TILDEO_ESTIMATE] = {}
+
+ try:
+ est[name][BASE_TILDEO_ESTIMATE][BASE_TIME] = algorithm.time_complexity()
+ except NotImplementedError:
+ est[name][BASE_TILDEO_ESTIMATE][BASE_TIME] = "--"
+ try:
+ est[name][BASE_TILDEO_ESTIMATE][BASE_MEMORY] = algorithm.memory_complexity()
+ except NotImplementedError:
+ est[name][BASE_TILDEO_ESTIMATE][BASE_MEMORY] = "--"
+ try:
+ est[name][BASE_TILDEO_ESTIMATE][BASE_PARAMETERS] = algorithm.get_optimal_parameters_dict()
+ except NotImplementedError:
+ est[name][BASE_TILDEO_ESTIMATE][BASE_PARAMETERS] = "--"
+
+ def _add_quantum_complexity(self, algorithm: BaseAlgorithm):
+ """
+ runs the quantum time analysis for the given `algorithm`
+
+ INPUT:
+
+ - ``algorithm`` -- Algorithm to run.
+
+ """
+ est = self.estimates
+ name = algorithm.__class__.__name__
+ try:
+ est[name][BASE_QUANTUMO] = {}
+ est[name][BASE_QUANTUMO][BASE_TIME] = algorithm.quantum_time_complexity()
+ except NotImplementedError:
+ est[name][BASE_QUANTUMO][BASE_TIME] = "--"
+
+ def _add_estimate(self, algorithm: BaseAlgorithm):
+ """
+ runs the bit security analysis for the given `algorithm`
+
+ INPUT:
+
+ - ``algorithm`` -- Algorithm to run.
+
+ """
+ est = self.estimates
+ name = algorithm.__class__.__name__
+ algorithm.complexity_type = ComplexityType.ESTIMATE.value
+ est[name][BASE_ESTIMATEO] = {}
+
+ est[name][BASE_ESTIMATEO][BASE_TIME] = algorithm.time_complexity() if not isinf(
+ algorithm.time_complexity()) else '--'
+
+ est[name][BASE_ESTIMATEO][BASE_MEMORY] = algorithm.memory_complexity() if not isinf(
+ algorithm.memory_complexity()) else '--'
+
+ est[name][BASE_ESTIMATEO][BASE_PARAMETERS] = algorithm.get_optimal_parameters_dict()
+
+ est[name][BASE_ADDITIONALO] = algorithm._get_verbose_information(
+ ) if not isinf(algorithm.time_complexity()) else {}
+
+ def estimate(self, **kwargs):
+ """
+ Returns dictionary describing the complexity of each algorithm and its optimal parameters
+
+ """
+ logger = kwargs.get("logger", None)
+
+ if not self.estimates:
+ self.estimates = dict()
+ for index, algorithm in enumerate(self.algorithms()):
+ name = algorithm.__class__.__name__
+ if name not in self.estimates:
+ self.estimates[name] = {}
+
+ # used only in the GUI
+ if logger:
+ logger(
+ f"[{str(index + 1)}/{str(self.nalgorithms())}] - Processing algorithm: '{name}'")
+
+ if self.include_tildeo and BASE_TILDEO_ESTIMATE not in self.estimates[name]:
+ self._add_tilde_o_complexity(algorithm)
+
+ if self.include_quantum and BASE_QUANTUMO not in self.estimates[name]:
+ self._add_quantum_complexity(algorithm)
+
+ if BASE_ESTIMATEO not in self.estimates[name]:
+ self._add_estimate(algorithm)
+
+ return self.estimates
+
+ def table(self, show_quantum_complexity=False, show_tilde_o_time=False, show_all_parameters=False, precision=1, truncate=False):
+ """
+ Print table describing the complexity of each algorithm and its optimal parameters
+
+ INPUT:
+
+ - ``show_quantum_complexity`` -- show quantum time complexity (default: false)
+ - ``show_tilde_o_time`` -- show Ε time complexity (default: false)
+ - ``show_all_parameters`` -- show all optimization parameters (default: false)
+ - ``precision`` -- number of decimal digits output (default: 1)
+ - ``truncate`` -- truncate rather than round the output (default: false)
+
+ """
+ self.include_tildeo = show_tilde_o_time
+ self.include_quantum = show_quantum_complexity
+ estimate = self.estimate()
+
+ if estimate == {}:
+ raise ValueError(
+ "No algorithm associated with this estimator or applicable to this problem instance.")
+
+ else:
+ renderer = EstimationRenderer(
+ show_quantum_complexity, show_tilde_o_time, show_all_parameters, precision, truncate
+ )
+ renderer.as_table(estimate)
+
+ def fastest_algorithm(self, use_tilde_o_time=False):
+ """
+ Return the algorithm with the smallest time complexity
+
+ INPUT:
+
+ - ``use_tilde_o_time`` -- use Ε time complexity, i.e., ignore polynomial factors (default: False)
+ """
+ if use_tilde_o_time:
+ self.complexity_type = ComplexityType.TILDEO.value
+
+ def key(algorithm):
+ return algorithm.time_complexity()
+
+ return min(self.algorithms(), key=key)
+
+ def reset(self):
+ """
+ Resets the internal states of the estimator and all included algorithms
+
+ """
+
+ self.estimates = {}
+ for i in self.algorithms():
+ i.reset()
diff --git a/cryptographic_estimators/base_problem.py b/cryptographic_estimators/base_problem.py
new file mode 100644
index 00000000..2f6cb628
--- /dev/null
+++ b/cryptographic_estimators/base_problem.py
@@ -0,0 +1,66 @@
+# ****************************************************************************
+# 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 math import inf
+
+
+class BaseProblem(object):
+ """
+ Construct an instance of BaseProblem
+
+ INPUT:
+
+ - ``parameters`` -- dict of parameters of the problem.
+ - ``nsolutions`` -- number of solutions of the problem
+ - ``memory_bound`` -- maximum allowed memory to use for solving the problem (default: inf)
+
+ """
+
+ def __init__(self, **kwargs):
+ self.parameters = {}
+ self.nsolutions = None
+ self.memory_bound = inf if "memory_bound" not in kwargs else kwargs["memory_bound"]
+
+ def expected_number_solutions(self):
+ """
+ Returns the expected number of existing solutions to the problem
+
+ """
+ return NotImplementedError
+
+ def to_bitcomplexity_time(self, basic_operations: float):
+ """
+ Returns the bit-complexity associated to a given number of basic-operations
+
+ INPUT:
+
+ - ``basic_operations`` -- number of basic operations (logarithmic)
+
+ """
+ return NotImplementedError
+
+ def to_bitcomplexity_memory(self, elements_to_store: float):
+ """
+ Returns the memory bit-complexity associated to a given number of elements to store
+
+ INPUT:
+
+ - ``elements_to_store`` -- number of memory elements (logarithmic)
+
+ """
+ return NotImplementedError
diff --git a/cryptographic_estimators/estimation_renderer.py b/cryptographic_estimators/estimation_renderer.py
new file mode 100644
index 00000000..c31da9f8
--- /dev/null
+++ b/cryptographic_estimators/estimation_renderer.py
@@ -0,0 +1,133 @@
+# ****************************************************************************
+# 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_constants import BASE_ALGORITHM, BASE_PARAMETERS, BASE_TIME, BASE_MEMORY, BASE_ADDITIONALO, BASE_QUANTUMO, BASE_TILDEO_ESTIMATE
+from .helper import concat_all_tables, round_or_truncate
+from copy import deepcopy
+from prettytable import PrettyTable
+from sage.all import RR
+
+
+class EstimationRenderer():
+ def __init__(self, show_quantum_complexity=0, show_tilde_o_time=0, show_all_parameters=0, precision=1, truncate=0) -> None:
+ """
+ Creates an estimation renderer
+ INPUT:
+
+ - ``show_quantum_complexity`` -- show quantum time complexity (default: false)
+ - ``show_tilde_o_time`` -- show Ε time complexity (default: false)
+ - ``show_all_parameters`` -- show all optimization parameters (default: false)
+ - ``precision`` -- number of decimal digits output (default: 1)
+ - ``truncate`` -- truncate rather than round the output (default: false)
+ """
+ self._show_quantum_complexity = show_quantum_complexity
+ self._show_tilde_o_time = show_tilde_o_time
+ self._show_all_parameters = show_all_parameters
+ self._precision = precision
+ self._truncate = truncate
+
+ def as_table(self, estimation_result: dict) -> None:
+ """
+ Prints the given estimation dictionary as a table
+ """
+ estimation = deepcopy(estimation_result)
+ if estimation == {}:
+ raise ValueError("No algorithms associated with this estimator.")
+
+ key = list(estimation.keys())[0]
+ tables = []
+ tbl = self._create_initial_table_containing_algorithm_column(
+ estimation)
+ tables.append(tbl)
+
+ for j in estimation[key].keys().__reversed__():
+ if j == BASE_QUANTUMO and not self._show_quantum_complexity:
+ continue
+ if j == BASE_TILDEO_ESTIMATE and not self._show_tilde_o_time:
+ continue
+ if j == BASE_ADDITIONALO:
+ continue
+
+ tbl = self._create_subtable_containing_all_columns(j, estimation)
+ self._add_rows(tbl, estimation)
+ tables.append(tbl)
+
+ if j == BASE_QUANTUMO:
+ tbl._min_width = {BASE_TIME: len(BASE_QUANTUMO)}
+
+ tbl_join = concat_all_tables(tables)
+
+ print(tbl_join)
+
+ def _create_initial_table_containing_algorithm_column(self, estimation: dict) -> PrettyTable:
+ """
+ creates a `PrettyTable` with the analysis results, containg
+ - expected runtime and memory
+ - optimal parameters
+
+ """
+ table = PrettyTable([BASE_ALGORITHM])
+ table.padding_width = 1
+ table.title = ' '
+ table.align[BASE_ALGORITHM] = "l"
+
+ for i in estimation.keys():
+ table.add_row([i])
+
+ return table
+
+ def _create_subtable_containing_all_columns(self, sub_table_name: str, estimation: dict):
+ """
+ Creates a `PrettyTable` subtable.
+
+ INPUT:
+
+ - ``sub_table_name`` -- TODO
+ - ``estimation`` the estimation dictionary containing the results
+ """
+ algorithm_name = list(estimation.keys())[0]
+ table_columns = [
+ i for i in list(estimation[algorithm_name][sub_table_name].keys())
+ if i != BASE_PARAMETERS or self._show_all_parameters
+ ]
+ table = PrettyTable(table_columns, min_table_width=len(sub_table_name))
+ table.padding_width = 1
+ table.title = sub_table_name
+ if BASE_TIME in table_columns:
+ table.align[BASE_TIME] = "r"
+ if BASE_MEMORY in table_columns:
+ table.align[BASE_MEMORY] = "r"
+ return table
+
+ def _add_rows(self, sub_table: PrettyTable, estimation: dict) -> PrettyTable:
+ """
+
+ INPUT:
+
+ - ``tbl`` -- current `PrettyTable` table
+ - ``truncate`` -- bool: if set the value will be truncated
+ - ``estimation`` the estimation dictionary containing the results
+
+ """
+ for i in estimation.keys():
+ row = [estimation[i][sub_table.title][k]
+ for k in sub_table.field_names]
+ row = [round_or_truncate(
+ 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/cryptographic_estimators/helper.py b/cryptographic_estimators/helper.py
new file mode 100644
index 00000000..0160520e
--- /dev/null
+++ b/cryptographic_estimators/helper.py
@@ -0,0 +1,87 @@
+# ****************************************************************************
+# 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 math import log2
+from enum import Enum
+
+
+class ComplexityType(Enum):
+ """
+ distinguish between normal optimisation and tilde O optimisation
+ """
+ ESTIMATE = 0
+ TILDEO = 1
+
+
+def concat_all_tables(tables):
+ """
+
+ INPUT:
+
+ - ``tables`` -- list of `PrettyTable`
+ """
+ tbl_join = concat_pretty_tables(str(tables[0]), str(tables[1]))
+ for i in range(2, len(tables)):
+ tbl_join = concat_pretty_tables(tbl_join, str(tables[i]))
+ return tbl_join
+
+
+def concat_pretty_tables(t1: str, t2: str):
+ """
+ Merge two columns into one
+ INPUT:
+
+ - ``t1`` -- first column
+ - ``t2`` -- second column
+
+ """
+ v = t1.split("\n")
+ v2 = t2.split("\n")
+ vnew = ""
+ for i in range(len(v)):
+ vnew += v[i] + v2[i][1:] + "\n"
+ return vnew[:-1]
+
+
+def _truncate(x: float, precision: int):
+ """
+ truncate a value
+
+ INPUT:
+
+ - ``x`` -- value to truncate
+ - ``precision`` -- number of decimial digits to truncate to
+
+ """
+ return float(int(x * 10 ** precision) / 10 ** precision)
+
+
+def round_or_truncate(x: float, truncate: bool, precision: int):
+ """
+ eiter rounds or truncates `x` if `truncate` is `true`
+
+ INPUT:
+
+ - ``x`` -- value to either truncate or round
+ - ``truncate`` -- if `true`: `x` will be truncated else rounded
+ - ``precision`` -- number of decimial digits
+
+ """
+ val = _truncate(x, precision) if truncate \
+ else round(float(x), precision)
+ return '{:.{p}f}'.format(val, p=precision)
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
new file mode 100644
index 00000000..129d0eae
--- /dev/null
+++ b/docs/CONTRIBUTING.md
@@ -0,0 +1,73 @@
+## Project structure
+This the current project structure.
+```sh
+ββ cryptographic_estimators
+β βββ base_algorithm.py
+β βββ base_estimator.py
+β βββ base_problem.py
+β βββ helper.py
+β βββ MQEstimator
+β β βββ degree_of_regularity.py
+β β βββ mq_algorithm.py
+β β βββ MQAlgorithms
+β β βββ mq_estimator.py
+β β βββ mq_helper.py
+β β βββ mq_problem.py
+β β βββ series
+β β βββ witness_degree.py
+β βββ SDEstimator
+β βββ sd_algorithm.py
+β βββ SDAlgorithms
+β βββ sd_estimator.py
+β βββ sd_helper.py
+β βββ sd_problem.py
+```
+If you want to add a new estimator please run `make add-estimator` and it will create the basic code and folder structure for you to edit, you also can review the `DummyEstimator` to see a minimal reproduction of whats its needed to start.
+
+````python
+ββ cryptographic_estimators
+ β βββ base_algorithm.py
+ β βββ base_problem.py
+ β βββ base_estimator.py
+ β βββ NEWEstimator
+ β βββ NEWestimator.py (Inherits from base_estimator)
+ β βββ NEWproblem.py (Inherits from base_problem)
+ β βββ NEWalgorithm.py (Inherits from base_algorithm)
+ β βββ Algorithms
+ β βββ List of algorithms (Inherits from NEWalgorithm.py)
+````
+---
+## GIT Conventions
+### Commits
+To contribute to this project please follow the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/).
+
+### Branching
+Branch names should be snake_case. Which means that all the text must be lowercase and replace spaces with dashes. Also we should add as a prefix based on the type of implementation. For example:
+
+```
+poc/some_testing_branch
+refactor/modify_base_problem
+feature/implement_dummy_estimator
+fix/algorithm_parameter
+```
+
+### Pull request
+ 1. Only create pull requests to the `develop` branch.
+
+---
+
+### Testing
+#### Unit tests
+To build and run the image based on Dockerfile.test
+```sh
+make test
+```
+or if you have Apple Silicon M1 Chip
+```sh
+make test-m1
+```
+
+### Documenting
+Remember to document your code using [sphinx syntax](https://www.sphinx-doc.org/en/master/tutorial/automatic-doc-generation.html).
+
+
diff --git a/docs/images/favicon.ico b/docs/images/favicon.ico
new file mode 100644
index 00000000..2fd7c47e
Binary files /dev/null and b/docs/images/favicon.ico differ
diff --git a/docs/images/tii_logo.png b/docs/images/tii_logo.png
new file mode 100644
index 00000000..5eed2f75
Binary files /dev/null and b/docs/images/tii_logo.png differ
diff --git a/input_dictionary.json b/input_dictionary.json
new file mode 100644
index 00000000..48e2974c
--- /dev/null
+++ b/input_dictionary.json
@@ -0,0 +1,958 @@
+{
+ "estimators": [
+ {
+ "estimator_id": "SDEstimator",
+ "algorithm_id": "SDAlgorithm",
+ "display_label": "Binary Syndrome Decoding",
+ "landing_page_content": "# Binary Syndrome Decoding Estimator\n\n\nThis project provides an estimator for the hardness of the binary syndrome decoding problem. This problem is defined as follows:\n\nLet $\\mathbf H\\in\\mathbb{F}_2^{(n-k)\\times n}$ be the parity-check matrix of a code of length $n$ and dimension $k$. Given $\\mathbf H$, a syndrome $\\mathbf{s}\\in\\mathbb{F}_2^{n-k}$ and an integer $\\omega < n$ the syndrome decoding problem asks to find a vector $\\mathbf e \\in \\mathbb{F}_2^n$ satisfying $\\mathbf H \\mathbf e=\\mathbf s$ while $\\mathbf e$ has a Hamming weight smaller or equal to $\\omega$.\n\nThe estimator covers Information Set Decoding (ISD) algorithms to estimate the hardness of given instances. More details on the theoretical foundations of the estimator can be found in the corresponding paper:\n\n*[[EB22]](https://eprint.iacr.org/2021/1243.pdf) Andre Esser and Emanuele Bellini. Syndrome Decoding Estimator. In Public-Key Cryptography (PKC 2022), 25th IACR International Conference on Practice and Theory of Public-Key Cryptography*\n\n",
+ "problem_parameters": [
+ {
+ "id": "n",
+ "type": "number",
+ "display_label": "Code length",
+ "placeholder": "Insert parameter",
+ "tooltip": "Code length of the specified code",
+ "validate_fields_ids": [
+ "k",
+ "w"
+ ]
+ },
+ {
+ "id": "k",
+ "type": "number",
+ "display_label": "Code dimension",
+ "placeholder": "Insert parameter",
+ "tooltip": "Code dimension of specified code",
+ "validate_fields_ids": [
+ "w"
+ ],
+ "dependencies": [
+ {
+ "id": "n",
+ "action": "validateLessThan"
+ }
+ ]
+ },
+ {
+ "id": "w",
+ "type": "number",
+ "display_label": "Error weight",
+ "placeholder": "Insert parameter",
+ "tooltip": "Hamming weight of the target solution",
+ "dependencies": [
+ {
+ "id": "n",
+ "action": "validateLessThan"
+ },
+ {
+ "action": "validateLessThanOperation",
+ "operation": "n - k"
+ }
+ ]
+ }
+ ],
+ "estimator_parameters": [
+ {
+ "id": "bit_complexities",
+ "type": "switch",
+ "display_label": "Bit complexities",
+ "default_value": true,
+ "tooltip": "Show complexities as count of bit operations. If false, show number of field additions"
+ },
+ {
+ "id": "hmap",
+ "type": "switch",
+ "display_label": "Hashmap",
+ "default_value": true,
+ "tooltip": "Use linear time matching between lists via hashmaps. If false, use sort-and-match"
+ },
+ {
+ "id": "included_algorithms",
+ "type": "multiple_selector",
+ "direction": "column",
+ "display_label": "Included algorithms",
+ "tooltip": "Algorithms to include for optimization",
+ "default_value": [],
+ "excluded_algorithms": [
+ "BJMMdw",
+ "BJMMpdw"
+ ],
+ "options": [],
+ "dependencies": [
+ {
+ "id": "limit_depth",
+ "parent_contains": [
+ "MayOzerov",
+ "BJMM"
+ ],
+ "action": "show"
+ }
+ ]
+ },
+ {
+ "id": "workfactor_accuracy",
+ "type": "slider",
+ "direction": "column",
+ "display_label": "Tilde-O accuracy",
+ "default_value": 1,
+ "tooltip": "Accuracy level of optimization",
+ "min": 1,
+ "max": 100,
+ "number_of_decimals": 0,
+ "step": 1
+ },
+ {
+ "id": "limit_depth",
+ "type": "switch",
+ "display_label": "Limit tree-depth",
+ "default_value": false,
+ "tooltip": "Limits the depth of May-Ozerov and BJMM algorithm to two, otherwise two and three are considered"
+ }
+ ],
+ "optional_parameters": [
+ {
+ "id": "memory_bound",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Memory limit",
+ "default_value": null,
+ "placeholder": "Insert value",
+ "caption": "Leave empty if no limit is desired",
+ "tooltip": "Log2 of the maximum number of bits of memory available"
+ },
+ {
+ "id": "precision",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Decimal precision",
+ "default_value": 0,
+ "placeholder": "Insert value",
+ "tooltip": "Number of decimal digits to display"
+ },
+ {
+ "id": "include_tildeo",
+ "type": "switch",
+ "display_label": "Tilde-O complexity",
+ "default_value": false,
+ "tooltip": "Include complexity estimates that disregard polynomial factors",
+ "dependencies": [
+ {
+ "id": "workfactor_accuracy",
+ "parent_value": true,
+ "action": "show"
+ }
+ ]
+ },
+ {
+ "id": "nsolutions",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Number of solutions",
+ "placeholder": "Insert value",
+ "tooltip": "Log2 of number of existing solutions of which one has to be found"
+ },
+ {
+ "id": "include_quantum",
+ "type": "switch",
+ "display_label": "Quantum complexity",
+ "default_value": false,
+ "tooltip": "Include quantum complexity for chosen algorithms, if implemented",
+ "dependencies": [
+ {
+ "id": "quantum_maxdepth",
+ "parent_value": true,
+ "action": "show"
+ }
+ ]
+ },
+ {
+ "id": "quantum_maxdepth",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Maximum depth",
+ "default_value": 96.0,
+ "placeholder": "Insert value",
+ "tooltip": "Maximum allowed depth of the quantum circuit"
+ },
+ {
+ "id": "memory_access",
+ "type": "selector",
+ "direction": "column",
+ "display_label": "Memory access cost",
+ "default_value": "Constant",
+ "tooltip": "Function that takes as input the memory bit complexity and outputs the associate algorithmic cost. Example, logarithmic memory access, input M, output M+log2M.",
+ "options": [
+ "Constant",
+ "Logaritmic",
+ "Square root",
+ "Cube root"
+ ]
+ }
+ ]
+ },
+ {
+ "estimator_id": "MQEstimator",
+ "algorithm_id": "MQAlgorithm",
+ "display_label": "Multivariate Quadratic",
+ "landing_page_content": "# Multivariate Quadratic Estimator\n\nThis project provides an estimator for the hardness of the multivariate quadratic problem. This problem is defined as follows: \n\nLet $\\mathbb{F}_{q}$ be a field with $q$ elements, and let $\\mathbb{F}_{q}[x_1, x_2, \\ldots, x_n]$ be the ring of polynomials in the variables $x_1, x_2, \\ldots, x_n$ and coefficients in $\\mathbb{F}_{q}$. Given $p_1, p_2, \\ldots, p_m \\in \\; \\mathbb{F}_{q}[x_1, x_2, \\ldots, x_n]$ the multivariate quadratic problem asks to find a vector $\\mathbf a \\in \\; \\mathbb{F}_q^n$ satisfying $p_{i}(\\mathbf a ) = 0$ for all $i=1,2,\\ldots,m$. \n\nMore details on the theoretical foundations of the estimator can be found in the corresponding paper: \n\n*[[BMSV22]](https://eprint.iacr.org/2022/708.pdf) Emanuele Bellini, Rusydi H. Makarim, Carlo Sanna, and Javier Verbel. An Estimator for the Hardness of the MQ Problem. AFRICACRYPT 2022, 13th International Conference on Cryptography.*\n",
+ "problem_parameters": [
+ {
+ "id": "n",
+ "type": "number",
+ "display_label": "Number of variables",
+ "placeholder": "Insert parameter",
+ "tooltip": "The number of variables of the system to solve",
+ "validate_fields_ids": [
+ "h"
+ ]
+ },
+ {
+ "id": "m",
+ "type": "number",
+ "display_label": "Number of equations",
+ "placeholder": "Insert parameter",
+ "tooltip": "The number of equations of the system to solve"
+ },
+ {
+ "id": "q",
+ "type": "number",
+ "display_label": "Field size",
+ "placeholder": "Insert parameter",
+ "tooltip": "An integer of the form p^x for some integer x, where p is prime number. This value indicates the number of elements in the underlying field"
+ }
+ ],
+ "estimator_parameters": [
+ {
+ "id": "w",
+ "type": "slider",
+ "direction": "column",
+ "display_label": "Matrix multiplication constant",
+ "default_value": 2.0,
+ "tooltip": "Indicates that two square matrices of size n can be multiplied by performing O(n^w) field multiplications",
+ "min": 2,
+ "max": 3,
+ "number_of_decimals": 2,
+ "step": 0.01
+ },
+ {
+ "id": "theta",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Bitcompexity exponent",
+ "default_value": 2,
+ "placeholder": "Insert value",
+ "tooltip": "The bitcomplexity of a field multiplication is assumed to be log_2(q)^(theta). Note that for theta = 0 the output gives the (log of the) necessary number of field multiplications."
+ },
+ {
+ "id": "h",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Number of variables to guess",
+ "default_value": 0,
+ "placeholder": "Insert parameter",
+ "tooltip": "Assumes that the initial system is splited into q^h subsystems of n-h variables and m equations. In this case the time complexity is given by q^h times the complexity a given subsystem. The memory complexity is given by the memory required to solve one subsystem",
+ "dependencies": [
+ {
+ "id": "n",
+ "action": "validateLessThan"
+ }
+ ]
+ },
+ {
+ "id": "included_algorithms",
+ "type": "multiple_selector",
+ "direction": "column",
+ "display_label": "Included algorithms",
+ "tooltip": "Algorithms to include for optimization",
+ "default_value": [],
+ "excluded_algorithms": [],
+ "options": [],
+ "dependencies": [
+ {
+ "id": "max_D",
+ "parent_contains": [
+ "Crossbred"
+ ],
+ "action": "show"
+ }
+ ]
+ },
+ {
+ "id": "max_D",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Max D for Crossbred",
+ "default_value": 20,
+ "placeholder": "Insert value",
+ "tooltip": "Upper bound to the parameter D "
+ }
+ ],
+ "optional_parameters": [
+ {
+ "id": "memory_bound",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Memory limit",
+ "default_value": null,
+ "placeholder": "Insert value",
+ "caption": "Leave empty if no limit is desired",
+ "tooltip": "Log2 of the maximum number of bits of memory available"
+ },
+ {
+ "id": "precision",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Decimal precision",
+ "default_value": 0,
+ "placeholder": "Insert value",
+ "tooltip": "Number of decimal digits to display"
+ },
+ {
+ "id": "include_tildeo",
+ "type": "switch",
+ "display_label": "Tilde-O complexity",
+ "default_value": false,
+ "tooltip": "Include complexity estimates that disregard polynomial factors"
+ },
+ {
+ "id": "nsolutions",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Number of solutions",
+ "default_value": 0,
+ "placeholder": "Insert value",
+ "tooltip": "Log2 of number of existing solutions of which one has to be found"
+ },
+ {
+ "id": "include_quantum",
+ "type": "switch",
+ "display_label": "Quantum complexity",
+ "default_value": false,
+ "tooltip": "Include quantum complexity for chosen algorithms, if implemented",
+ "dependencies": [
+ {
+ "id": "quantum_maxdepth",
+ "parent_value": false,
+ "action": "show"
+ },
+ {
+ "id": "w",
+ "parent_value": true,
+ "action": "show"
+ }
+ ]
+ },
+ {
+ "id": "quantum_maxdepth",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Maximum depth",
+ "default_value": 96.0,
+ "placeholder": "Insert value",
+ "tooltip": "Maximum allowed depth of the quantum circuit"
+ },
+ {
+ "id": "memory_access",
+ "type": "selector",
+ "direction": "column",
+ "display_label": "Memory access cost",
+ "default_value": "Constant",
+ "tooltip": "Function that takes as input the memory bit complexity and outputs the associate algorithmic cost. Example, logarithmic memory access, input M, output M+log2M.",
+ "options": [
+ "Constant",
+ "Logaritmic",
+ "Square root",
+ "Cube root"
+ ]
+ }
+ ]
+ },
+ {
+ "estimator_id": "DummyEstimator",
+ "algorithm_id": "DummyAlgorithm",
+ "display_label": "Dummy",
+ "landing_page_content": "# User can write here Markdown \n and TeX content.",
+ "problem_parameters": [
+ {
+ "id": "Parameter1",
+ "type": "number",
+ "display_label": "Parameter 1",
+ "placeholder": "Insert parameter",
+ "tooltip": "This is the first problem parameter",
+ "validate_fields_ids": [
+ "Parameter2"
+ ]
+ },
+ {
+ "id": "Parameter2",
+ "type": "number",
+ "display_label": "Parameter 2",
+ "placeholder": "Insert parameter",
+ "tooltip": "This is the first problem parameter",
+ "dependencies": [
+ {
+ "id": "Parameter1",
+ "action": "validateLessThan"
+ }
+ ]
+ }
+ ],
+ "estimator_parameters": [
+ {
+ "id": "bit_complexities",
+ "type": "switch",
+ "display_label": "Bit complexities",
+ "default_value": true,
+ "tooltip": "Show complexities as count of bit operations. If false, show number of elementary operations"
+ },
+ {
+ "id": "included_algorithms",
+ "type": "multiple_selector",
+ "direction": "column",
+ "display_label": "Included algorithms",
+ "tooltip": "Algorithms to include for optimization",
+ "default_value": [],
+ "excluded_algorithms": [],
+ "options": [],
+ "dependencies": []
+ }
+ ],
+ "optional_parameters": [
+ {
+ "id": "memory_bound",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Memory limit",
+ "default_value": null,
+ "placeholder": "Insert value",
+ "caption": "Leave empty if no limit is desired",
+ "tooltip": "Log2 of the maximum number of bits of memory available"
+ },
+ {
+ "id": "precision",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Decimal precision",
+ "default_value": 0,
+ "placeholder": "Insert value",
+ "tooltip": "Number of decimal digits to display"
+ },
+ {
+ "id": "include_tildeo",
+ "type": "switch",
+ "display_label": "Tilde-O complexity",
+ "default_value": false,
+ "tooltip": "Include complexity estimates that disregard polynomial factors",
+ "dependencies": []
+ },
+ {
+ "id": "nsolutions",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Number of solutions",
+ "placeholder": "Insert value",
+ "tooltip": "Log2 of number of existing solutions of which one has to be found"
+ },
+ {
+ "id": "include_quantum",
+ "type": "switch",
+ "display_label": "Quantum complexity",
+ "default_value": false,
+ "tooltip": "Include quantum complexity for chosen algorithms, if implemented",
+ "dependencies": [
+ {
+ "id": "quantum_maxdepth",
+ "parent_value": true,
+ "action": "show"
+ }
+ ]
+ },
+ {
+ "id": "quantum_maxdepth",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Maximum depth",
+ "default_value": 96.0,
+ "placeholder": "Insert value",
+ "tooltip": "Maximum allowed depth of the quantum circuit"
+ },
+ {
+ "id": "memory_access",
+ "type": "selector",
+ "direction": "column",
+ "display_label": "Memory access cost",
+ "default_value": "Constant",
+ "tooltip": "Function that takes as input the memory bit complexity and outputs the associate algorithmic cost. Example, logarithmic memory access, input M, output M+log2M.",
+ "options": [
+ "Constant",
+ "Logaritmic",
+ "Square root",
+ "Cube root"
+ ]
+ }
+ ]
+ },
+ {
+ "estimator_id": "SDFQEstimator",
+ "algorithm_id": "SDFQAlgorithm",
+ "display_label": "Syndrome Decoding over Fq",
+ "landing_page_content": "# Syndrome Decoding Estimator over $\\mathbf F_q$\n\n\nThis project provides an estimator for the hardness of the syndrome decoding problem over $\\mathbf F_q$. This problem is defined as follows:\n\nLet $\\mathbf H\\in\\mathbb{F}_q^{(n-k)\\times n}$ be the parity-check matrix of a code of length $n$ and dimension $k$. Given $\\mathbf H$, a syndrome $\\mathbf{s}\\in\\mathbb{F}_q^{n-k}$ and an integer $\\omega < n$ the syndrome decoding problem asks to find a vector $\\mathbf e \\in \\mathbb{F}_q^n$ satisfying $\\mathbf H \\mathbf e=\\mathbf s$ while $\\mathbf e$ has a Hamming weight smaller or equal to $\\omega$.\n\nThe estimator covers Information Set Decoding (ISD) algorithms to estimate the hardness of given instances.",
+ "problem_parameters": [
+ {
+ "id": "n",
+ "type": "number",
+ "display_label": "Code length",
+ "placeholder": "Insert parameter",
+ "tooltip": "Code length of the specified code",
+ "validate_fields_ids": [
+ "k",
+ "w"
+ ]
+ },
+ {
+ "id": "k",
+ "type": "number",
+ "display_label": "Code dimension",
+ "placeholder": "Insert parameter",
+ "tooltip": "Code dimension of specified code",
+ "validate_fields_ids": [
+ "w"
+ ],
+ "dependencies": [
+ {
+ "id": "n",
+ "action": "validateLessThan"
+ }
+ ]
+ },
+ {
+ "id": "w",
+ "type": "number",
+ "display_label": "Error weight",
+ "placeholder": "Insert parameter",
+ "tooltip": "Hamming weight of the target solution",
+ "dependencies": [
+ {
+ "id": "n",
+ "action": "validateLessThan"
+ },
+ {
+ "action": "validateLessThanOperation",
+ "operation": "n - k"
+ }
+ ]
+ },
+ {
+ "id": "q",
+ "type": "number",
+ "display_label": "Base Field Size",
+ "placeholder": "Insert parameter",
+ "tooltip": "Size of the underlying base field",
+ "dependencies": []
+ },
+ {
+ "id": "memory_bound",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Memory limit",
+ "default_value": null,
+ "placeholder": "Insert value",
+ "caption": "Leave empty if no limit is desired",
+ "tooltip": "Log2 of the maximum number of bits of memory available"
+ },
+ {
+ "id": "nsolutions",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Number of solutions",
+ "placeholder": "Insert value",
+ "tooltip": "Log2 of number of existing solutions of which one has to be found, if not specified it will be fixed to the number of solutions in expectation"
+ }
+ ],
+ "estimator_parameters": [
+ {
+ "id": "bit_complexities",
+ "type": "switch",
+ "display_label": "Bit complexities",
+ "default_value": true,
+ "tooltip": "Show complexities as count of bit operations. If false, show number of field additions"
+ },
+ {
+ "id": "included_algorithms",
+ "type": "multiple_selector",
+ "direction": "column",
+ "display_label": "Included algorithms",
+ "tooltip": "Algorithms to include for optimization",
+ "default_value": [],
+ "excluded_algorithms": [],
+ "options": [],
+ "dependencies": []
+ },
+ {
+ "id": "memory_access",
+ "type": "selector",
+ "direction": "column",
+ "display_label": "Memory access cost",
+ "default_value": "Constant",
+ "tooltip": "Function that takes as input the memory bit complexity and outputs the associate algorithmic cost. Example, logarithmic memory access, input M, output M+log2M.",
+ "options": [
+ "Constant",
+ "Logaritmic",
+ "Square root",
+ "Cube root"
+ ]
+ },
+ {
+ "id": "precision",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Decimal precision",
+ "default_value": 0,
+ "placeholder": "Insert value",
+ "tooltip": "Number of decimal digits to display"
+ }
+ ],
+ "optional_parameters": [
+ {
+ "id": "is_syndrome_zero",
+ "type": "selector",
+ "direction": "column",
+ "display_label": "Code word search",
+ "default_value": 1,
+ "tooltip": "Indicates if a small code word is searched, i.e., if the syndrome is equal to the zero.",
+ "options": [
+ "Syndrome decoding",
+ "Code word search"
+ ]
+ }
+ ]
+ },
+ {
+ "estimator_id": "PKEstimator",
+ "algorithm_id": "PKAlgorithm",
+ "display_label": "Permuted Kernel",
+ "landing_page_content": " # Permuted Kernel Estimator\n\nThis project provides an estimator for the hardness of the permuted kernel problem. This problem is defined as follows: \n\nGiven two matrices $\\mathbf{A} \\in \\; \\mathbb{F}_{q}^{m\\times n}$ and $\\mathbf{V} \\in \\; \\mathbb{F}_{q}^{\\ell \\times n}$, the permuted kernel problem asks to find a permutation $\\mathbf{P} \\in \\mathbb{F}_{q}^{m\\times n}$ such that $\\mathbf{A}(\\mathbf{V} \\mathbf{P})^\\top = 0$\n\nMore details on the theoretical foundations of the estimator can be found in the corresponding papers: \n\n[[SBC22]](https://eprint.iacr.org/2022/1749.pdf) Paolo Santini, Marco Baldi, and Franco Chiaraluce. Computational hardness of the permuted kernel and subcode equivalence problems. \n\n*[[KMP19]](https://eprint.iacr.org/2019/412.pdf) Eliane Koussa, Gilles Macario-Rat and Jacques Patarin. On the complexity of the permuted kernel problem. Cryptology ePrint Archive, Report 2019/412 (2019).*",
+ "problem_parameters": [
+ {
+ "id": "n",
+ "type": "number",
+ "display_label": "Number of columns",
+ "placeholder": "Insert parameter",
+ "tooltip": "Number of columns of the input matrix"
+ },
+ {
+ "id": "m",
+ "type": "number",
+ "display_label": "Number of rows",
+ "placeholder": "Insert parameter",
+ "tooltip": "Number of rows of the input matrix"
+ },
+ {
+ "id": "q",
+ "type": "number",
+ "display_label": "Field size",
+ "placeholder": "Insert parameter",
+ "tooltip": "A prime number"
+ },
+ {
+ "id": "ell",
+ "type": "number",
+ "display_label": "No. of rows in the kernel",
+ "placeholder": "Insert value",
+ "default_value": 1,
+ "tooltip": "Number of rows of the matrix whose permutation should lie in the kernel"
+ },
+ {
+ "id": "use_parity_row",
+ "type": "switch",
+ "display_label": "Use parity row",
+ "default_value": false,
+ "tooltip": "Enables trick of appending extra (all one) row to the matrix, i.e., m -> m+1"
+ },
+ {
+ "id": "memory_bound",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Memory limit",
+ "placeholder": "Insert value",
+ "caption": "Leave empty if no limit is desired",
+ "tooltip": "Log2 of the maximum number of bits of memory available"
+ },
+ {
+ "id": "nsolutions",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Number of solutions",
+ "placeholder": "Insert value",
+ "tooltip": "Log2 of number of existing solutions of which one has to be found"
+ }
+ ],
+ "estimator_parameters": [
+ {
+ "id": "bit_complexities",
+ "type": "switch",
+ "display_label": "Bit complexities",
+ "default_value": true,
+ "tooltip": "Show complexities as count of bit operations. If false, show number of elementary operations"
+ },
+ {
+ "id": "included_algorithms",
+ "type": "multiple_selector",
+ "direction": "column",
+ "display_label": "Included algorithms",
+ "tooltip": "Algorithms to include for optimization",
+ "default_value": [],
+ "excluded_algorithms": [],
+ "options": [],
+ "dependencies": []
+ },
+ {
+ "id": "memory_access",
+ "type": "selector",
+ "direction": "column",
+ "display_label": "Memory access cost",
+ "default_value": "Constant",
+ "tooltip": "Function that takes as input the memory bit complexity and outputs the associate algorithmic cost. Example, logarithmic memory access, input M, output M+log2M.",
+ "options": [
+ "Constant",
+ "Logaritmic",
+ "Square root",
+ "Cube root"
+ ]
+ },
+ {
+ "id": "precision",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Decimal precision",
+ "default_value": 0,
+ "placeholder": "Insert value",
+ "tooltip": "Number of decimal digits to display"
+ }
+ ],
+ "optional_parameters": [
+ {
+ "id": "cost_for_list_operation",
+ "type": "number",
+ "display_label": "List operation cost (Time)",
+ "direction": "column",
+ "placeholder": "Insert value",
+ "tooltip": "Cost in Fq additions for one list operation in the SBC and KMP algorithms"
+ },
+ {
+ "id": "memory_for_list_element",
+ "type": "number",
+ "display_label": "List operation cost (Memory)",
+ "direction": "column",
+ "placeholder": "Insert value",
+ "tooltip": "Memory in Fq elements for one list element in the SBC and KMP algorithms"
+ }
+ ]
+ },
+ {
+ "estimator_id": "PEEstimator",
+ "algorithm_id": "PEAlgorithm",
+ "display_label": "Permutation Equivalence",
+ "landing_page_content": "# Permuted Equivalence Estimator\n\nThis project provides an estimator for the hardness of the permuted equivalence problem. \n\nThis problem is defined as follows: Given two matrices $\\mathbf{G}, \\mathbf{G}' \\in \\; \\mathbb{F}_{q}^{k\\times n}$, the permuted equivalence problem asks to find an invertible matrix $\\mathbf{S} \\in \\; \\mathbb{F}_{q}^{k\\times n}$ and permutation matrix $\\mathbf{P} \\in \\; \\mathbb{F}_{q}^{n\\times n}$ such that $\\mathbf{G} = \\mathbf{S}\\mathbf{G}\\mathbf{P}$.\n\nMore details on the theoretical foundations of the estimator can be found in the corresponding papers: \n\n*[[Beu20]](https://eprint.iacr.org/2020/801.pdf) Ward Beullens. Not enough LESS: An improved algorithm for solving code equivalence problems over Fq. In: Dunkelman, O., Jr., M.J.J., OβFlynn, C. (eds.) SAC 2020.*\n\n*[[Leo82]](https://ieeexplore.ieee.org/document/1056498) J. Leon. Computing automorphism groups of error-correcting codes. IEEE Transactions on Information Theory 28(3), 496β511 (1982).*\n\n*[[Sen97]](https://epubs.siam.org/doi/10.1137/S0895480195294027) Nicolas Sendrier. On the dimension of the hull. SIAM Journal on Discrete Mathematics 10(2), 282β293 (1997).*\n\n\n",
+ "problem_parameters": [
+ {
+ "id": "n",
+ "type": "number",
+ "display_label": "Code length",
+ "placeholder": "Insert parameter",
+ "tooltip": "Code length of the specified code",
+ "validate_fields_ids": [
+ "k"
+ ]
+ },
+ {
+ "id": "k",
+ "type": "number",
+ "display_label": "Code dimension",
+ "placeholder": "Insert parameter",
+ "tooltip": "Code dimension of specified code",
+ "dependencies": [
+ {
+ "id": "n",
+ "action": "validateLessThan"
+ }
+ ]
+ },
+ {
+ "id": "q",
+ "type": "number",
+ "display_label": "Field size",
+ "placeholder": "Insert parameter",
+ "tooltip": "A prime number"
+ },
+ {
+ "id": "h",
+ "type": "number",
+ "display_label": "Hull dimension",
+ "placeholder": "Insert parameter",
+ "tooltip": "The dimension of the hull"
+ },
+ {
+ "id": "memory_bound",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Memory limit",
+ "placeholder": "Insert value",
+ "caption": "Leave empty if no limit is desired",
+ "tooltip": "Log2 of the maximum number of bits of memory available"
+ },
+ {
+ "id": "nsolutions",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Number of solutions",
+ "placeholder": "Insert value",
+ "tooltip": "Log2 of number of existing solutions of which one has to be found"
+ }
+ ],
+ "estimator_parameters": [
+ {
+ "id": "bit_complexities",
+ "type": "switch",
+ "display_label": "Bit complexities",
+ "default_value": true,
+ "tooltip": "Show complexities as count of bit operations. If false, show number of elementary operations"
+ },
+ {
+ "id": "included_algorithms",
+ "type": "multiple_selector",
+ "direction": "column",
+ "display_label": "Included algorithms",
+ "tooltip": "Algorithms to include for optimization",
+ "default_value": [],
+ "excluded_algorithms": [],
+ "options": [],
+ "dependencies": []
+ },
+ {
+ "id": "memory_access",
+ "type": "selector",
+ "direction": "column",
+ "display_label": "Memory access cost",
+ "default_value": "Constant",
+ "tooltip": "Function that takes as input the memory bit complexity and outputs the associate algorithmic cost. Example, logarithmic memory access, input M, output M+log2M.",
+ "options": [
+ "Constant",
+ "Logaritmic",
+ "Square root",
+ "Cube root"
+ ]
+ },
+ {
+ "id": "precision",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Decimal precision",
+ "default_value": 0,
+ "placeholder": "Insert value",
+ "tooltip": "Number of decimal digits to display"
+ }
+ ],
+ "optional_parameters":[]
+ },
+ {
+ "estimator_id": "LEEstimator",
+ "algorithm_id": "LEAlgorithm",
+ "display_label": "Linear Equivalence",
+ "landing_page_content": "# Linear Equivalence Estimator\n\nThis project provides an estimator for the hardness of the linear equivalence problem. \n\nThis problem is defined as follows: Given two matrices $\\mathbf{G}, \\mathbf{G}' \\in \\; \\mathbb{F}_{q}^{k\\times n}$, the linear equivalence problem asks to find an invertible matrix $\\mathbf{S} \\in \\; \\mathbb{F}_{q}^{k\\times n}$ and monomial matrix $\\mathbf{Q} \\in \\; \\mathbb{F}_{q}^{n\\times n}$ such that $\\mathbf{G} = \\mathbf{S}\\mathbf{G}\\mathbf{P}$.\n\nMore details on the theoretical foundations of the estimator can be found in the corresponding papers: \n\n*[[BBPS22]](https://eprint.iacr.org/2022/967.pdf) Alessandro Barenghi, Jean-Francois Biasse, Edoardo Persichetti and Paolo Santini,. On the Computational Hardness of the Code Equivalence Problem in Cryptography.*\n\n*[[Beu20]](https://eprint.iacr.org/2020/801.pdf) Ward Beullens. Not enough LESS: An improved algorithm for solving code equivalence problems over Fq. In: Dunkelman, O., Jr., M.J.J., OβFlynn, C. (eds.) SAC 2020.*\n\n*[[Leo82]](https://ieeexplore.ieee.org/document/1056498) J. Leon. Computing automorphism groups of error-correcting codes. IEEE Transactions on Information Theory 28(3), 496β511 (1982).*",
+ "problem_parameters": [
+ {
+ "id": "n",
+ "type": "number",
+ "display_label": "Code length",
+ "placeholder": "Insert parameter",
+ "tooltip": "Code length of the specified code",
+ "validate_fields_ids": [
+ "k"
+ ]
+ },
+ {
+ "id": "k",
+ "type": "number",
+ "display_label": "Code dimension",
+ "placeholder": "Insert parameter",
+ "tooltip": "Code dimension of specified code",
+ "dependencies": [
+ {
+ "id": "n",
+ "action": "validateLessThan"
+ }
+ ]
+ },
+ {
+ "id": "q",
+ "type": "number",
+ "display_label": "Field size",
+ "placeholder": "Insert parameter",
+ "tooltip": "A prime number"
+ },
+ {
+ "id": "memory_bound",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Memory limit",
+ "placeholder": "Insert value",
+ "caption": "Leave empty if no limit is desired",
+ "tooltip": "Log2 of the maximum number of bits of memory available"
+ },
+ {
+ "id": "nsolutions",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Number of solutions",
+ "placeholder": "Insert value",
+ "tooltip": "Log2 of number of existing solutions of which one has to be found"
+ }
+ ],
+ "estimator_parameters": [
+ {
+ "id": "bit_complexities",
+ "type": "switch",
+ "display_label": "Bit complexities",
+ "default_value": true,
+ "tooltip": "Show complexities as count of bit operations. If false, show number of elementary operations"
+ },
+ {
+ "id": "included_algorithms",
+ "type": "multiple_selector",
+ "direction": "column",
+ "display_label": "Included algorithms",
+ "tooltip": "Algorithms to include for optimization",
+ "default_value": [],
+ "excluded_algorithms": [],
+ "options": [],
+ "dependencies": []
+ },
+ {
+ "id": "memory_access",
+ "type": "selector",
+ "direction": "column",
+ "display_label": "Memory access cost",
+ "default_value": "Constant",
+ "tooltip": "Function that takes as input the memory bit complexity and outputs the associate algorithmic cost. Example, logarithmic memory access, input M, output M+log2M.",
+ "options": [
+ "Constant",
+ "Logaritmic",
+ "Square root",
+ "Cube root"
+ ]
+ },
+ {
+ "id": "precision",
+ "type": "number",
+ "direction": "column",
+ "display_label": "Decimal precision",
+ "default_value": 0,
+ "placeholder": "Insert value",
+ "tooltip": "Number of decimal digits to display"
+ }
+ ],
+ "optional_parameters":[]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/references.rst b/references.rst
new file mode 100644
index 00000000..61c982c4
--- /dev/null
+++ b/references.rst
@@ -0,0 +1,262 @@
+.. _references:
+
+References
+==========
+
+:ref:`A `
+:ref:`B `
+:ref:`C `
+:ref:`D `
+:ref:`E `
+:ref:`F `
+:ref:`G `
+:ref:`H `
+:ref:`I `
+:ref:`J `
+:ref:`K `
+:ref:`L `
+:ref:`M `
+:ref:`N `
+:ref:`O `
+:ref:`P `
+:ref:`Q `
+:ref:`R `
+:ref:`S `
+:ref:`T `
+:ref:`U `
+:ref:`V `
+:ref:`W `
+:ref:`X `
+:ref:`Y `
+:ref:`Z `
+
+.. _ref-A:
+
+**A**
+
+.. [BKW19] \Andreas BjΓΆrklund, Petteri Kaski, and Ryan Williams.
+ *Solving Systems of Polynomial Equations over GF(2) by a Parity-Counting Self-Reduction*
+ https://drops.dagstuhl.de/opus/volltexte/2019/10602/pdf/LIPIcs-ICALP-2019-26.pdf
+
+.. [JV18] \Antoine Joux and Vanessa Vitse.
+ *A Crossbred Algorithm for Solving Boolean Polynomial Systems*
+ https://link.springer.com/chapter/10.1007/978-3-319-76620-1_1
+
+.. [KPG99] \Aviad Kipnis, Jacques Patarin, and Louis Goubin.
+ *Unbalanced Oil and Vinegar Signature Schemes*
+ https://link.springer.com/chapter/10.1007/3-540-48910-X_15
+
+.. [BBPS20] \Alessandro Barenghi, Jean-Francois Biasse, Edoardo Persichetti and Paolo Santini.
+ *{LESS-FM:} Fine-Tuning Signatures from the Code Equivalence Problem*
+ https://doi.org/10.1007/978-3-030-81293-5\_2
+
+.. [EZ23] \Andre Esser and Floyd Zweydinger.
+ *New Time-Memory Trade-Offs for Subset Sum -- Improving ISD in Theory and Practice*
+ https://eprint.iacr.org/2022/1329.pdf
+
+.. [EB22] \Andre Esser and Emanuele Bellini.
+ *Syndrome Decoding Estimator*
+ https://eprint.iacr.org/2021/1243.pdf
+
+.. [MO15] \Alexander May and Ilya Ozerov.
+ *On computing nearest neighbors with applications to decoding of binary linear codes*
+ https://link.springer.com/chapter/10.1007/978-3-662-46800-5_9
+
+.. [MMT11] \Alexander May, Alexander Meurer and Enrico Thomae.
+ *Decoding random linear codes in 2^(0.054n)*
+ https://link.springer.com/chapter/10.1007/978-3-642-25385-0_6
+
+.. [BJMM12] \Anja Becker, Antoine Joux, Alexander May and Alexander Meurer.
+ *Decoding random binary linear codes in 2^(n/20): How 1+ 1= 0 improves information set decoding*
+ https://eprint.iacr.org/2012/026.pdf
+.. _ref-B:
+
+**B**
+
+.. _ref-C:
+
+**C**
+
+.. [Pet11] \Christiane Peters: Information-set decoding for linear codes over Fq.
+ https://link.springer.com/chapter/10.1007/978-3-642-12929-2_7
+
+.. [BCCCNSY10] \Charles Bouillaguet, Hsieh-Chung Chen, Chen-Mou Cheng, Tung Chou, Ruben Niederhagen, Adi Shamir, and Bo-Yin Yang.
+ *Fast Exhaustive Search for Polynomial Systems in F_2* https://www.iacr.org/archive/ches2010/62250195/62250195.pdf
+
+.. _ref-D:
+
+**D**
+
+.. [BLP08] Daniel J. Bernstein and Tanja Lange and Christiane Peters
+ *Attacking and Defending the McEliece Cryptosystem*
+ https://link.springer.com/chapter/10.1007/978-3-540-88403-3_3
+
+.. [BLP11] Daniel J. Bernstein and Tanja Lange and Christiane Peters
+ *Smaller decoding exponents: ball-collision decoding*
+ https://link.springer.com/chapter/10.1007/978-3-642-22792-9_42
+
+.. [LPTWY17] \Daniel Lokshtanov, Ramamohan Paturi, Suguru Tamaki, Ryan Williams, and Huacheng Yu.
+ *Beating Brute Force for Systems of Polynomial Equation over Finite Fields*
+ https://people.csail.mit.edu/rrw/polyEqSoda2017Submit.pdf
+
+.. _ref-E:
+
+**E**
+
+.. [KMP19] \Eliane Koussa, Gilles Macario{-}Rat and Jacques Patarin.
+ *On the complexity of the Permuted Kernel Problem*
+ https://eprint.iacr.org/2019/412.pdf
+
+.. [Pra62] \Eugene Prange.
+ *The use of information sets in decoding cyclic codes*
+ https://doi.org/10.1109/TIT.1962.1057777
+
+.. _ref-F:
+
+**F**
+
+.. _ref-G:
+
+**G**
+
+.. _ref-H:
+
+**H**
+
+.. [MHT13] \Hiroyuki Miura, Yasufumi Hashimoto, and Tsuyoshi Takagi.
+ *Extended Algorithm for Solving Underdefined Multivariate Quadratic Equations*
+ https://link.springer.com/chapter/10.1007/978-3-642-38616-9_8
+
+.. _ref-I:
+
+**I**
+
+.. [Dum91] \Ilya Dumer.
+ *On minimum distance decoding of linear codes*
+
+.. [Din21a] \Itai Dinur.
+ *Improved Algorithms for Solving Polynomial Systems over GF(2) by Multiple Parity-Counting*
+ https://arxiv.org/pdf/2005.04800.pdf
+
+.. [Din21b] \Itai Dinur.
+ *Cryptanalytic Applications of the Polynomial Method for Solving Multivariate Equation Systems over GF(2)*
+ https://eprint.iacr.org/2021/578.pdf
+
+.. _ref-J:
+
+**J**
+
+.. [Ste88] \Jacques Stern.
+ *A method for finding codewords of small weight*
+ https://doi.org/10.1007/BFb0019850
+
+.. [Dua20] \JoΓ£o Diogo Duarte.
+ *On the Complexity of the Crossbred Algorithm*
+
+.. [Leo82] \JJeffrey S. Leon.
+ *Computing automorphism groups of error-correcting codes*
+ https://doi.org/10.1109/TIT.1982.1056498
+
+.. _ref-K:
+
+**K**
+
+.. _ref-L:
+
+**L**
+
+.. [BFP09] \Luk Bettale, Jean-Charles Faugère and Ludovic Perret.
+ *Hybrid Approach for Solving Multivariate Systems over Finite Fields*
+ https://doi.org/10.1515/JMC.2009.009
+
+.. [BFP12] \Luk Bettale, Jean-Charles Faugère and Ludovic Perret.
+ *Solving Polynomial Systems over Finite Fields: Improved Analysis of the Hybrid Approach*
+ https://doi.org/10.1145/2442829.2442843
+
+.. [BM18] \Leif Both and Alexander May.
+ *Decoding Linear Codes with High Error Rate and its Impact for LPN Security*
+ https://eprint.iacr.org/2017/1139.pdf
+
+.. _ref-M:
+
+**M**
+
+.. [BFSS11] \Magali Bardet, Jean-Charles Faugère, Bruno Salvy, and Pierre-Jean Spaenlehauer.
+ *On the Complexity of Solving Quadratic Boolean Systems*
+ https://www.sciencedirect.com/science/article/pii/S0885064X12000611
+
+.. _ref-N:
+
+**N**
+
+.. [CKPS] \Nicolas Courtois, Alexander Klimov, Jacques Patarin, and Adi Shamir.
+ *Efficient Algorithms for Solving Overdefined Systems of Multivariate Polynomial Equations*
+ https://www.iacr.org/archive/eurocrypt2000/1807/18070398-new.pdf
+
+.. [CGMT02] \Nicolas Courtois, Louis Goubin, Willi Meier, and Jean-Daniel Tacier.
+ *Solving Underdefined Systems of Multivariate Quadratic Equations*
+ https://link.springer.com/chapter/10.1007/3-540-45664-3_15
+
+.. [Sen06] \Nicolas Sendrier
+ *The Support Splitting Algorithm*
+ https://hal.inria.fr/inria-00073037/document
+
+.. _ref-O:
+
+**O**
+
+.. _ref-P:
+
+**P**
+
+
+.. [LB88] \Pil Joong Lee and Ernest Brickell.
+ *An observation on the security of McElieceβs public-key cryptosystem*
+ https://doi.org/10.1007/3-540-45961-8\_25
+
+.. [SBC22] \Paolo Santini, Marco Baldi, and Franco Chiaraluce
+ *Computational Hardness of the Permuted Kernel and Subcode Equivalence Problems*
+ https://eprint.iacr.org/2022/1749.pdf
+
+.. _ref-Q:
+
+**Q**
+
+.. _ref-R:
+
+**R**
+
+.. _ref-S:
+
+**S**
+
+.. _ref-T:
+
+**T**
+
+.. _ref-U:
+
+**U**
+
+.. _ref-V:
+
+**V**
+
+.. _ref-W:
+
+**W**
+
+.. [Beu20] \Ward Beullens.
+ *Not enough LESS: An improved algorithm for solving Code Equivalence Problems over Fq*
+ https://doi.org/10.1007/978-3-030-81652-0\_15
+.. _ref-X:
+
+**X**
+
+.. _ref-Y:
+
+**Y**
+
+.. _ref-Z:
+
+**Z**
diff --git a/scripts/__init__.py b/scripts/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/scripts/append_estimator_to_input_dictionary.py b/scripts/append_estimator_to_input_dictionary.py
new file mode 100644
index 00000000..bca1aad8
--- /dev/null
+++ b/scripts/append_estimator_to_input_dictionary.py
@@ -0,0 +1,74 @@
+"""
+This module appends the structure for the new estimator into the input_dicitonary.json
+"""
+
+
+class AppendEstimator():
+ def __init__(self):
+ estimator_prefix = input(
+ "Enter a prefix for your Estimator (For example for SyndromeDecoding we use SD): ")
+ self.estimator_prefix = estimator_prefix
+ self.estimator_label = input("Enter the estimator full name: ")
+ self.upper_estimator_prefix = self.estimator_prefix.upper()
+ self.lower_estimator_prefix = self.estimator_prefix.lower()
+
+ def write(self):
+ f = open("./input_dictionary.json", "rb+")
+ f.seek(-8, 2)
+ new_estimator_content = "},\n" + self.get_estimator_content()
+ f.write(new_estimator_content.encode('ascii'))
+ f.close()
+
+ def get_estimator_content(self):
+ tabs = "\t" * 2
+ template = f"{tabs}"+"{\n" + \
+ f"{tabs}" + f"\"estimator_id\": \"{self.upper_estimator_prefix}Estimator\",\n" + \
+ f"{tabs}" + f"\"algorithm_id\": \"{self.upper_estimator_prefix}Algorithm\",\n" + \
+ f"{tabs}" + f"\"display_label\": \"{self.estimator_label}\",\n" + \
+ f"{tabs}" + "\"landing_page_content\": \"# Dummy Markdown + Tex Landing Page\",\n" + \
+ self.get_problem_parameters() + \
+ self.get_estimators_parameters() + \
+ f"{tabs}" + "\"optional_parameters\":[]\n" + \
+ f"{tabs}" + "}\n" \
+ "\t]\n" \
+ "}"
+ return template
+
+ def get_problem_parameters(self):
+ tabs = "\t"
+ template = f"{tabs}" * 2 + "\"problem_parameters\": [\n" + \
+ f"{tabs}" * 3 + "{\n" + \
+ f"{tabs}" * 4 + "\"id\": \"Parameter1\",\n" + \
+ f"{tabs}" * 4 + "\"type\": \"number\",\n" + \
+ f"{tabs}" * 4 + "\"display_label\": \"Parameter 1\",\n" + \
+ f"{tabs}" * 4 + "\"placeholder\": \"Insert parameter\",\n" + \
+ f"{tabs}" * 4 + "\"tooltip\": \"This is the first problem parameter\"\n" + \
+ f"{tabs}" * 3 + "}\n" + \
+ f"{tabs}" * 2 + "],\n"
+ return template
+
+ def get_estimators_parameters(self):
+ tabs = "\t"
+ template = f"{tabs}" * 2 + "\"estimator_parameters\": [\n" + \
+ f"{tabs}" * 3 + "{\n" + \
+ f"{tabs}" * 4 + "\"id\": \"included_algorithms\",\n" + \
+ f"{tabs}" * 4 + "\"type\": \"multiple_selector\",\n" + \
+ f"{tabs}" * 4 + "\"direction\": \"column\",\n" + \
+ f"{tabs}" * 4 + "\"display_label\": \"Included algorithms\",\n" + \
+ f"{tabs}" * 4 + "\"tooltip\": \"Algorithms to include for optimization\",\n" + \
+ f"{tabs}" * 4 + "\"default_value\": [],\n" + \
+ f"{tabs}" * 4 + "\"excluded_algorithms\": [],\n" + \
+ f"{tabs}" * 4 + "\"options\": [],\n" + \
+ f"{tabs}" * 4 + "\"dependencies\": []\n" + \
+ f"{tabs}" * 3 + "}\n" + \
+ f"{tabs}" * 2 + "],\n"
+ return template
+
+ def get_algorithms_init_content(self):
+ template = f"from .sample import Sample\n" + \
+ "# TODO: Remember to add the algorithms to the import above"
+ return template
+
+
+ae = AppendEstimator()
+ae.write()
diff --git a/scripts/create_copyright.py b/scripts/create_copyright.py
new file mode 100644
index 00000000..cbdcaa74
--- /dev/null
+++ b/scripts/create_copyright.py
@@ -0,0 +1,33 @@
+import os
+
+COPYRIGHT = "# ****************************************************************************\n" + \
+"# Copyright 2023 Technology Innovation Institute\n" + \
+"# \n" + \
+"# This program is free software: you can redistribute it and/or modify\n" + \
+"# it under the terms of the GNU General Public License as published by\n" + \
+"# the Free Software Foundation, either version 3 of the License, or\n" + \
+"# (at your option) any later version.\n" + \
+"# \n" + \
+"# This program is distributed in the hope that it will be useful,\n" + \
+"# but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + \
+"# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + \
+"# GNU General Public License for more details.\n" + \
+"# \n" + \
+"# You should have received a copy of the GNU General Public License\n" + \
+"# along with this program. If not, see .\n" + \
+"# ****************************************************************************\n "
+
+ROOT_FOLDER = "cryptographic_estimators"
+FILES_TO_READ = os.walk(ROOT_FOLDER)
+EXCLUDED_FILES = ["__init__.py"]
+
+for root, directories, files in FILES_TO_READ:
+ for file in files:
+ if file.endswith('.py') and file not in EXCLUDED_FILES:
+ current_file_path = os.path.join(root, file)
+ with open(current_file_path, 'r+') as current_file:
+ file_content = current_file.read()
+ current_file.seek(0, 0)
+ if COPYRIGHT not in file_content:
+ current_file.write(f'{COPYRIGHT}\n\n{file_content}')
+ current_file.truncate()
\ No newline at end of file
diff --git a/scripts/create_documentation.py b/scripts/create_documentation.py
new file mode 100644
index 00000000..165d6efe
--- /dev/null
+++ b/scripts/create_documentation.py
@@ -0,0 +1,98 @@
+import os
+from shutil import copyfile
+from pathlib import Path
+
+EXCLUDED_FOLDERS = ["__pycache__"]
+EXCLUDED_FILES = ["__init__.py", "mq_main.sage.py",
+ "references.rst", "scipy_model.py"]
+EXCLUDED_EXTENSIONS = [".md", ".so", ".dylib", ".egg-info", ".pyc", ".sage"]
+SOURCE_ROOT_FOLDER = "./docs/source/"
+Path(SOURCE_ROOT_FOLDER).mkdir(exist_ok=True)
+
+
+def header_style(section, level):
+ if not section:
+ return ""
+
+ sections = {
+ 0: "=",
+ 1: "-",
+ 2: "=",
+ 3: "-",
+ 4: "`",
+ 5: "'",
+ 6: ".",
+ 7: "~",
+ 8: "*",
+ 9: "+",
+ 10: "^"
+ }
+ style = sections[level] * len(section)
+
+ if level in [0, 1]:
+ return style + "\n" + section + "\n" + style + "\n"
+
+ if level in [2, 3, 4, 5, 6, 7, 8, 9, 10]:
+ return section + "\n" + style + "\n"
+
+ return section
+
+
+with Path(SOURCE_ROOT_FOLDER, "index.rst").open(mode="w") as index_rst_file:
+ index_rst_file.write("=========================\n"
+ "TII Cryptanalysis Library\n"
+ "=========================\n"
+ "\n"
+ "This is a sample reference manual for TII cryptanalysis library.\n"
+ "\n"
+ "To use this module, you need to import it:: \n\n"
+ " from cryptographic_estimators import *\n\n"
+ "This reference shows a minimal example of documentation of the\n"
+ "TII cryptanalysis library following SageMath guidelines.\n")
+
+ ROOT_FOLDER = 'cryptographic_estimators/'
+
+ for root, directories, files in os.walk(ROOT_FOLDER):
+ path = root.split(os.sep)
+ folder_name = os.path.basename(root).replace("_", " ")
+ if os.path.basename(root) not in EXCLUDED_FOLDERS:
+ rst_folder = root.replace(ROOT_FOLDER, SOURCE_ROOT_FOLDER)
+ Path(rst_folder).mkdir(exist_ok=True)
+ index_rst_file.write(
+ f"{header_style(folder_name, len(path) - 1)}\n")
+ index_rst_file.write(".. toctree::\n\n")
+ for file in files:
+ file_name = os.path.splitext(file)[0]
+ file_extension = os.path.splitext(file)[1]
+ if file not in EXCLUDED_FILES and file_extension not in EXCLUDED_EXTENSIONS:
+ file_without_extension = os.path.splitext(file)[0]
+ file_path = os.path.join(root, file_without_extension)
+ index_rst_file.write(
+ f" {file_path.replace(ROOT_FOLDER, '')}\n")
+ with Path(rst_folder, file_name + ".rst").open(mode="w") as rst_file:
+ file_header = file_name.replace("_", " ")
+ adornment = "=" * len(file_header)
+ link = file_path.replace("/", ".")
+ rst_file.write(f"{header_style(file_name, 1)}\n"
+ f".. automodule:: {link}\n"
+ " :members:\n"
+ " :undoc-members:\n"
+ " :inherited-members:\n"
+ " :show-inheritance:\n\n")
+ index_rst_file.write(f"\n")
+
+ index_rst_file.write("\n\n"
+ "General Information\n"
+ "===================\n"
+ "\n"
+ "* :ref:`Bibliographic References `\n"
+ "\n"
+ "Indices and Tables\n"
+ "==================\n"
+ "\n"
+ "* :ref:`genindex`\n"
+ "* :ref:`modindex`\n"
+ "* :ref:`search`\n")
+
+copyfile("conf.py", Path(SOURCE_ROOT_FOLDER, "conf.py"))
+copyfile("references.rst", Path(SOURCE_ROOT_FOLDER, "references.rst"))
diff --git a/scripts/create_new_estimator.py b/scripts/create_new_estimator.py
new file mode 100644
index 00000000..9423fd52
--- /dev/null
+++ b/scripts/create_new_estimator.py
@@ -0,0 +1,74 @@
+"""
+This module contains a script that generates all the scaffolding
+code for a new estimator via the console.
+"""
+from os import path, mkdir, system
+from src.create_algorithm import CreateAlgorithm
+from src.create_estimator import CreateEstimator
+from src.create_problem import CreateProblem
+from src.create_specific_algorithm import CreateSpecificAlgorithm
+from src.create_init import CreateInit
+from src.create_constant import CreateConstants
+
+class EstimatorGenerator():
+ """
+ Creates all the files and folders for implementation ready estimator
+ """
+
+ def __init__(self):
+ estimator_prefix = input("Enter a prefix for your Estimator (For example for SyndromeDecoding we use SD): ")
+ self.estimator_folder_name = estimator_prefix.upper() + "Estimator"
+ self.algorithms_folder_name = estimator_prefix.upper() + "Algorithms"
+ self.estimator_path = ""
+ self.algorithms_path = ""
+ self.estimator_prefix = estimator_prefix
+
+ def create_estimator_folders(self):
+ """
+ Creates the estimator folder and the nested algorithms folder
+ """
+ print("# Creating folders...")
+ absolute_library_path = path.abspath("cryptographic_estimators")
+ self.estimator_path = path.join(absolute_library_path, self.estimator_folder_name)
+ self.algorithms_path = path.join(self.estimator_path, self.algorithms_folder_name)
+
+ self.__create_folder(self.estimator_path, self.estimator_folder_name)
+ self.__create_folder(self.algorithms_path, self.algorithms_folder_name)
+
+ def __create_folder(self, folder_path, folder_name):
+ try:
+ mkdir(folder_path, mode=0o777)
+ except OSError as error:
+ print(f"The directory {folder_name} already exits, please choose another name")
+ print(error)
+
+ def create_estimator_files(self):
+ """
+ Creates the Estimator,Problem, Algorithm and a Sample algorithm files
+ """
+ print("# Creating files...")
+ CreateProblem(self.estimator_prefix, self.estimator_path).write()
+ CreateEstimator(self.estimator_prefix, self.estimator_path).write()
+ CreateAlgorithm(self.estimator_prefix, self.estimator_path).write()
+ CreateConstants(self.estimator_prefix, self.estimator_path).write()
+ CreateSpecificAlgorithm(self.estimator_prefix, self.algorithms_path).write()
+
+ def create_init_files(self):
+ """
+ Create the init files for the package
+ """
+ print("# Creating init files...")
+ CreateInit(self.estimator_prefix).write_estimator_init(self.estimator_path)
+ CreateInit(self.estimator_prefix).write_algorithms_init(self.algorithms_path)
+
+ def done(self):
+ """Prints a done message"""
+ print(f"# Done! You can now start by editing the files inside '{self.estimator_path}' and the input_dictionary")
+ system('tree -d -I __pycache__ cryptographic_estimators')
+
+
+eg = EstimatorGenerator()
+eg.create_estimator_folders()
+eg.create_estimator_files()
+eg.create_init_files()
+eg.done()
diff --git a/scripts/src/__init__.py b/scripts/src/__init__.py
new file mode 100644
index 00000000..b81383c4
--- /dev/null
+++ b/scripts/src/__init__.py
@@ -0,0 +1,6 @@
+from .create_algorithm import CreateAlgorithm
+from .create_problem import CreateProblem
+from .create_estimator import CreateEstimator
+from .create_specific_algorithm import CreateSpecificAlgorithm
+from .create_init import CreateInit
+from .create_constant import CreateConstants
diff --git a/scripts/src/base_file_creator.py b/scripts/src/base_file_creator.py
new file mode 100644
index 00000000..0162a395
--- /dev/null
+++ b/scripts/src/base_file_creator.py
@@ -0,0 +1,31 @@
+from abc import ABC, abstractmethod
+
+
+class BaseFileCreator(ABC):
+ def __init__(self,estimator_prefix, estimator_path):
+ self.estimator_path = estimator_path
+ self.lowercase_prefix = estimator_prefix.lower()
+ self.uppercase_prefix = estimator_prefix.upper()
+
+ @abstractmethod
+ def write(self):
+ pass
+
+ @abstractmethod
+ def _get_file_content(self):
+ pass
+
+ @abstractmethod
+ def _create_imports(self):
+ pass
+
+ @abstractmethod
+ def _create_class(self):
+ pass
+
+ @abstractmethod
+ def _create_constructor(self):
+ pass
+
+ def _create_methods(self):
+ pass
diff --git a/scripts/src/create_algorithm.py b/scripts/src/create_algorithm.py
new file mode 100644
index 00000000..ead356a6
--- /dev/null
+++ b/scripts/src/create_algorithm.py
@@ -0,0 +1,44 @@
+"""
+This module generates the Algorithm class
+"""
+from .base_file_creator import BaseFileCreator
+
+class CreateAlgorithm(BaseFileCreator):
+
+ def write(self):
+ f = open(f"{self.estimator_path}/{self.lowercase_prefix}_algorithm.py", "w", encoding="utf8")
+ f.write(self._get_file_content())
+ f.close()
+
+ def _get_file_content(self):
+ """
+ Generates the file with the imports and the class definition
+ """
+ return self._create_imports() + self._create_class()
+
+ def _create_imports(self):
+ return "from ..base_algorithm import BaseAlgorithm, optimal_parameter\n\n\n"
+
+ def _create_class(self):
+ """
+ Generates the class with the constructor
+ """
+ template = f"class {self.uppercase_prefix}Algorithm(BaseAlgorithm):\n\n"
+ return template + self._create_constructor() + self._create_methods()
+
+ def _create_constructor(self):
+ """
+ Generates the __init__ method for the Algorithm class
+ """
+ template = "\tdef __init__(self, problem, **kwargs):\n" + \
+ "\t\tself._name = \"sample_name\"\n" + \
+ f"\t\tsuper({self.uppercase_prefix}Algorithm, self).__init__(problem, **kwargs)\n\n"
+ return template
+
+ def _create_methods(self):
+ """
+ Generates the methods to be ovewritten
+ """
+ template = "\tdef __repr__(self):\n" + \
+ "\t\tpass\n\n"
+ return template
diff --git a/scripts/src/create_constant.py b/scripts/src/create_constant.py
new file mode 100644
index 00000000..c889333e
--- /dev/null
+++ b/scripts/src/create_constant.py
@@ -0,0 +1,24 @@
+"""
+This module generates the Constants file
+"""
+from .base_file_creator import BaseFileCreator
+
+
+class CreateConstants(BaseFileCreator):
+
+ def write(self):
+ f = open(f"{self.estimator_path}/{self.lowercase_prefix}_constants.py", "w", encoding="utf8")
+ f.close()
+
+ def _get_file_content(self):
+ pass
+
+ def _create_imports(self):
+ pass
+
+ def _create_class(self):
+ pass
+
+ def _create_constructor(self):
+ pass
+
diff --git a/scripts/src/create_estimator.py b/scripts/src/create_estimator.py
new file mode 100644
index 00000000..1a4babfb
--- /dev/null
+++ b/scripts/src/create_estimator.py
@@ -0,0 +1,50 @@
+"""
+This module generates the Estimator class
+"""
+from .base_file_creator import BaseFileCreator
+
+class CreateEstimator(BaseFileCreator):
+
+ def write(self):
+ f = open(f"{self.estimator_path}/{self.lowercase_prefix}_estimator.py", "w", encoding="utf8")
+ f.write(self._get_file_content())
+ f.close()
+
+ def _get_file_content(self):
+ """
+ Generates the file with the imports and the class definition
+ """
+ return self._create_imports() + self._create_class()
+
+ def _create_imports(self):
+ """
+ Generates the imports at the top of the file
+ """
+ template = f"from ..{self.uppercase_prefix}Estimator.{self.lowercase_prefix}_algorithm import {self.uppercase_prefix}Algorithm\n" + \
+ f"from ..{self.uppercase_prefix}Estimator.{self.lowercase_prefix}_problem import {self.uppercase_prefix}Problem\n" + \
+ f"from ..base_estimator import BaseEstimator\n" + \
+ "from math import inf\n\n\n"
+ return template
+
+ def _create_class(self):
+ """
+ Generates the class with the constructor
+ """
+ template = f"class {self.uppercase_prefix}Estimator(BaseEstimator):\n" + \
+ "\t\"\"\" \n\n" + \
+ "\tINPUT:\n\n" + \
+ "\t- ``excluded_algorithms`` -- a list/tuple of excluded algorithms (default: None)\n\n" + \
+ "\t\"\"\" \n" + \
+ "\texcluded_algorithms_by_default = []\n\n"
+ return template + self._create_constructor()
+
+ def _create_constructor(self):
+ """
+ Generates the __init__ method for the Estimator class
+ """
+ template = "\tdef __init__(self, memory_bound=inf, **kwargs): # Add problem parameters\n" + \
+ "\t\tif not kwargs.get(\"excluded_algorithms\"):\n" + \
+ "\t\t\tkwargs[\"excluded_algorithms\"] = []\n\n" + \
+ "\t\tkwargs[\"excluded_algorithms\"] += self.excluded_algorithms_by_default\n" + \
+ f"\t\tsuper({self.uppercase_prefix}Estimator, self).__init__({self.uppercase_prefix}Algorithm, {self.uppercase_prefix}Problem(memory_bound=memory_bound, **kwargs), **kwargs)\n"
+ return template
diff --git a/scripts/src/create_init.py b/scripts/src/create_init.py
new file mode 100644
index 00000000..fbf25168
--- /dev/null
+++ b/scripts/src/create_init.py
@@ -0,0 +1,31 @@
+"""
+This module generates the init files for the Estimator and for the Algorithms of the estimator
+"""
+
+class CreateInit():
+ def __init__(self, estimator_prefix):
+ self.upper_estimator_prefix = estimator_prefix.upper()
+ self.lower_estimator_prefix = estimator_prefix.lower()
+
+ def write_estimator_init(self, estimator_path):
+ file = open(f"{estimator_path}/__init__.py", "w", encoding="utf8")
+ file.write(self.get_estimator_init_content())
+ file.close()
+
+ def get_estimator_init_content(self):
+ template = f"from .{self.lower_estimator_prefix}_algorithm import {self.upper_estimator_prefix}Algorithm\n" + \
+ f"from .{self.lower_estimator_prefix}_estimator import {self.upper_estimator_prefix}Estimator\n" + \
+ f"from .{self.lower_estimator_prefix}_problem import {self.upper_estimator_prefix}Problem\n" + \
+ f"from .{self.upper_estimator_prefix}Algorithms import Sample\n" + \
+ "# TODO: Remember to add the algorithms to the import above"
+ return template
+
+ def write_algorithms_init(self, algorithm_path):
+ file = open(f"{algorithm_path}/__init__.py", "w", encoding="utf8")
+ file.write(self.get_algorithms_init_content())
+ file.close()
+
+ def get_algorithms_init_content(self):
+ template = f"from .sample import Sample\n" + \
+ "# TODO: Remember to add the algorithms to the import above"
+ return template
diff --git a/scripts/src/create_problem.py b/scripts/src/create_problem.py
new file mode 100644
index 00000000..4b25b48a
--- /dev/null
+++ b/scripts/src/create_problem.py
@@ -0,0 +1,69 @@
+"""
+This module generates the Estimator class
+"""
+from .base_file_creator import BaseFileCreator
+
+class CreateProblem(BaseFileCreator):
+
+ def write(self):
+ f = open(f"{self.estimator_path}/{self.lowercase_prefix}_problem.py", "w", encoding="utf8")
+ f.write(self._get_file_content())
+ f.close()
+
+ def _get_file_content(self):
+ """
+ Generates the file with the imports and the class definition
+ """
+ return self._create_imports() + self._create_class()
+
+ def _create_imports(self):
+ return "from ..base_problem import BaseProblem\n\n\n"
+
+ def _create_class(self):
+ """
+ Generates the class with the constructor
+ """
+ template = f"class {self.uppercase_prefix}Problem(BaseProblem):\n\n"
+ return template + self._create_constructor() + self._create_methods()
+
+ def _create_constructor(self):
+ """
+ Generates the __init__ method for the Problem class
+ """
+ template = "\tdef __init__(self, **kwargs): # Fill with parameters\n" + \
+ "\t\tsuper().__init__(**kwargs)\n\n" + \
+ "\t\tself.nsolutions = kwargs.get(\"nsolutions\", max(self.expected_number_solutions(), 0))\n\n"
+ return template
+
+ def _create_methods(self):
+ """
+ Generates the methods to be ovewritten
+ """
+ template = "\tdef to_bitcomplexity_time(self, basic_operations):\n" + \
+ "\t\t\"\"\" \n" + \
+ "\t\tReturns the bit-complexity corresponding to basic_operations field additions\n\n" + \
+ "\t\tINPUT:\n\n" + \
+ "\t\t- ``basic_operations`` -- Number of field additions (logarithmic)\n\n" + \
+ "\t\t\"\"\" \n" + \
+ "\t\tpass\n\n" + \
+ "\tdef to_bitcomplexity_memory(self, elements_to_store):\n" + \
+ "\t\t\"\"\" \n" + \
+ "\t\tReturns the memory bit-complexity associated to a given number of elements to store\n\n" + \
+ "\t\tINPUT:\n\n" + \
+ "\t\t- ``elements_to_store`` -- number of elements to store (logarithmic)\n\n" + \
+ "\t\t\"\"\" \n" + \
+ "\t\tpass\n\n" + \
+ "\tdef expected_number_solutions(self):\n" + \
+ "\t\t\"\"\" \n" + \
+ "\t\t Returns the logarithm of the expected number of existing solutions to the problem\n\n" + \
+ "\t\t\"\"\" \n" + \
+ "\t\tpass\n\n" + \
+ "\tdef __repr__(self):\n" + \
+ "\t\t\"\"\" \n" + \
+ "\t\t\"\"\" \n" + \
+ "\t\tpass\n\n" + \
+ "\tdef get_parameters(self):\n" + \
+ "\t\t\"\"\" \n" + \
+ "\t\t\"\"\" \n" + \
+ "\t\tpass\n\n"
+ return template
diff --git a/scripts/src/create_specific_algorithm.py b/scripts/src/create_specific_algorithm.py
new file mode 100644
index 00000000..10abade5
--- /dev/null
+++ b/scripts/src/create_specific_algorithm.py
@@ -0,0 +1,47 @@
+"""
+This module generates the Estimator class
+"""
+from .base_file_creator import BaseFileCreator
+
+class CreateSpecificAlgorithm(BaseFileCreator):
+
+ def write(self):
+ f = open(f"{self.estimator_path}/sample.py", "w", encoding="utf8")
+ f.write(self._get_file_content())
+ f.close()
+
+ def _get_file_content(self):
+ """
+ Generates the file with the imports and the class definition
+ """
+ return self._create_imports() + self._create_class()
+
+ def _create_imports(self):
+ return f"from ...{self.uppercase_prefix}Estimator.{self.lowercase_prefix}_algorithm import {self.uppercase_prefix}Algorithm\n" + \
+ f"from ...{self.uppercase_prefix}Estimator.{self.lowercase_prefix}_problem import {self.uppercase_prefix}Problem\n\n\n"
+
+ def _create_class(self):
+ """
+ Generates the class with the constructor
+ """
+ template = f"class Sample({self.uppercase_prefix}Algorithm):\n\n"
+ return template + self._create_constructor() + self._create_methods()
+
+ def _create_constructor(self):
+ """
+ Generates the __init__ method for the Algorithm class
+ """
+
+ template = f"\tdef __init__(self, problem: {self.uppercase_prefix}Problem, **kwargs):\n" + \
+ "\t\tsuper().__init__(problem, **kwargs)\n\n"
+ return template
+
+ def _create_methods(self):
+ """
+ Generates the methods to be ovewritten
+ """
+ template = "\tdef _compute_time_complexity(self, parameters):\n" + \
+ "\t\tpass\n\n" + \
+ "\tdef _compute_memory_complexity(self, parameters):\n" + \
+ "\t\tpass\n\n"
+ return template
diff --git a/setup.py b/setup.py
new file mode 100644
index 00000000..2808ea2e
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,66 @@
+from setuptools import setup, find_packages
+from setuptools.command.test import test as TestCommand
+import multiprocessing
+import os
+import sys
+
+
+package_name = "cryptographic_estimators"
+
+# Get information from separate files (README, VERSION)
+
+
+def readfile(filename):
+ with open(filename, encoding='utf-8') as f:
+ return f.read()
+
+
+def run_tests(include_long_tests):
+ ncpus = multiprocessing.cpu_count()
+ timeout_in_sec = 3600 # 1 hour
+
+ SAGE_BIN = ""
+ if os.path.exists("SAGE_BIN_PATH"):
+ SAGE_BIN = readfile("SAGE_BIN_PATH").strip()
+
+ if len(SAGE_BIN) == 0:
+ SAGE_BIN = "sage"
+
+ long_test_flag = ""
+ if include_long_tests:
+ long_test_flag = "--long"
+
+ errno = os.system(
+ f"{SAGE_BIN} -t {long_test_flag} -T {timeout_in_sec} --nthreads {ncpus} --force-lib " + package_name)
+ if errno != 0:
+ sys.exit(1)
+
+# For the tests
+
+
+class SageTestFast(TestCommand):
+ def run_tests(self):
+ run_tests(include_long_tests=False)
+
+
+class SageTestAll(TestCommand):
+ def run_tests(self):
+ run_tests(include_long_tests=True)
+
+
+package_name = "cryptographic_estimators"
+packages = find_packages()
+setup(
+ name="cryptographic_estimators",
+ version="1.0.0",
+ author="TII",
+ description="This library provides bit security estimators and asymptotic complexity estimators for cyrptographic problems. So far it covers the binary Syndrome Decoding Problem (SDEstimator) and the Multivaritate Quadratic Problem (MQEstimator).",
+ packages=packages,
+ package_dir={package_name: package_name},
+ classifiers=[
+ "Programming Language :: Python :: 3",
+ "Operating System :: OS Independent",
+ ],
+ # adding a special setup command for tests
+ cmdclass={'testfast': SageTestFast, 'testall': SageTestAll},
+)
diff --git a/shell.nix b/shell.nix
new file mode 100644
index 00000000..808aaab2
--- /dev/null
+++ b/shell.nix
@@ -0,0 +1,50 @@
+with import {};
+let
+
+ # TODO combine those two
+ mysage = sage.override {
+ extraPythonPackages = ps: with ps; [
+ prettytable
+ scipy
+ sphinx
+ furo
+ autopep8
+ pip
+ pytest
+ ];
+ requireSageTests = false;
+ };
+
+ my-python = pkgs.python3;
+ mypython = my-python.withPackages (p: with p; [
+ prettytable
+ scipy
+ sphinx
+ furo
+ pip
+ autopep8
+ sage
+ pytest
+ ]);
+in
+{ pkgs ? import {} }:
+
+stdenv.mkDerivation {
+ name = "cryptographic_estimators";
+ src = ./.;
+
+ buildInputs = [
+ mypython
+ mysage
+ ripgrep
+ nodePackages.pyright
+ tree
+ ];
+
+ shellHook = ''
+ export PIP_PREFIX=$(pwd)/_build/pip_packages
+ export PYTHONPATH="$PIP_PREFIX/${mypython.sitePackages}:$PYTHONPATH"
+ export PATH="$PIP_PREFIX/bin:$PATH"
+ # unset SOURCE_DATE_EPOCH
+ '';
+}
diff --git a/tests/.gitignore b/tests/.gitignore
new file mode 100644
index 00000000..f104652b
--- /dev/null
+++ b/tests/.gitignore
@@ -0,0 +1 @@
+*.py
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/module/README.md b/tests/module/README.md
new file mode 100644
index 00000000..fb1532cf
--- /dev/null
+++ b/tests/module/README.md
@@ -0,0 +1,15 @@
+`attack_cost.py` originates from [Ward Beullens](https://github.com/WardBeullens/LESS_Attack)
+
+`cost.sage` originates from [LESS Project](https://github.com/paolo-santini/LESS_project)
+
+`kmp_cost.sage` originates from [KMP Attack](https://github.com/secomms/pkpattack/blob/main/kmp_cost.sage)
+
+`out_cost.sage` originates from [SBC Attack](https://github.com/secomms/pkpattack/blob/main/our_cost.sage)
+
+`cost_isd.sage` originates from [SBC Attack](https://github.com/secomms/pkpattack/blob/main/cost_isd.sage)
+
+`multivariate_quadratic_estimator` originates from [MQ Estimator](https://github.com/Crypto-TII/multivariate_quadratic_estimator)
+
+`estimator.py` originates from [SD Estimator](https://github.com/Crypto-TII/syndrome_decoding_estimator)
+
+`optimize.py` originates from [EZ Attack](https://github.com/FloydZ/Improving-ISD-in-Theory-and-Practice/blob/master/optimize.py)
\ No newline at end of file
diff --git a/tests/module/attack_cost.sage b/tests/module/attack_cost.sage
new file mode 100644
index 00000000..2bbb7d8c
--- /dev/null
+++ b/tests/module/attack_cost.sage
@@ -0,0 +1,138 @@
+#!/usr/bin/python3
+
+import random
+from math import factorial, log, sqrt, ceil
+import math
+
+def binomial(n,k):
+ return factorial(n)//factorial(k)//factorial(n-k)
+
+def random_sparse_vec_orbit(n,w,q):
+ counts = [0]*(q-1)
+ s = 0
+ while s q**(n-k)//ceil(4*log(n,2))) or (LEP == True and size_of_orbit > q**(2*(n-k))//ceil(4*log(n,2))) :
+ if verbose:
+ print("Orbit too big")
+ return None
+
+ list_size = sqrt(search_space_size*2*log(n,2))
+
+ #number of gaussian eliminations
+ GEs = 2*(binomial(n,W)/binomial(n-k,W-2)/binomial(k,2)/search_space_size)*list_size
+
+ if LEP == False: #number of row operations
+ ISD_cost = (k**2 + k*k*q)*GEs
+ else:
+ ISD_cost = (k*k + binomial(k,2))*GEs
+
+ if LEP == False:
+ Normal_form_cost = 2*list_size
+ else:
+ Normal_form_cost = 2*q*list_size
+
+ attack_cost = ISD_cost + Normal_form_cost
+
+ if verbose:
+ print("new attack cost (log_2 of #row ops): %f, list size: %d (2^%f), (w = %d)" % (log(attack_cost,2), list_size ,log(list_size,2),W))
+
+ return log(attack_cost,2)
+
+
+def attack_cost(n,k,q,verbose=False, LEP = True):
+ best_cost = 10000000
+ best_W = None
+
+ for W in range(1,n-k+1):
+ c = attack_cost_fixed_W(n,k,W,q,False,LEP)
+ if c is not None and c < best_cost:
+ best_cost = c
+ best_W = W;
+
+ if best_cost == 10000000:
+ return None
+
+ if verbose:
+ attack_cost_fixed_W(n,k,best_W,q,True,LEP)
+ return best_cost
+
+
+def minimal_w(n,k,q):
+ w = 1;
+ S = 0
+ while True:
+ S += binomial(n,w)*(q-1)**(w-1)
+ if S > 100*q**(n-k):
+ return w, ceil(S/q**(n-k))
+ w = w+1
+
+
+def ISD_COST(n,k,w,q):
+ #return (k*k + binomial(k,2)*q)*binomial(n,w)//binomial(n-k,w-2)//binomial(k,2)
+ return (k*k + k*k*q)*binomial(n,w)//binomial(n-k,w-2)//binomial(k,2)
+
+
+def LEON(n,k,q):
+ w , N = minimal_w(n,k,q)
+ S = ceil(2*(0.57+log(N)))*ISD_COST(n,k,w,q)
+
+ # print("Leon attack cost (log_2 of #row ops):%f, w: %d, N: %d " % (log(S,2),w,N), n, k, q)
+ return log(S,2);
+
+
+#print("proposed LESS-I parameters: ")
+#print(attack_cost(250,125,53))
+#LEON(250,125,53)
+#
+#print("\noriginal LESS-II parameters: ")
+#print(attack_cost(106,45,7))
+#LEON(106,45,7)
+#
+#print("\nproposed LESS-III parameters (larger field size): ")
+#print(attack_cost(280,117,149,LEP=False))
+#LEON(280,117,149)
+#
+#print("\nproposed LESS-III parameters (fixed field size): ")
+#print(attack_cost(305,127,31,LEP=False))
+#LEON(305,127,31)
diff --git a/tests/module/cost.sage b/tests/module/cost.sage
new file mode 100644
index 00000000..dd635679
--- /dev/null
+++ b/tests/module/cost.sage
@@ -0,0 +1,283 @@
+#reset();
+
+def log2(x):
+ return log(x*1.)/log(2.);
+
+#####################################
+
+def gauss_binomial(m,r,q):
+ x = 1;
+ for i in range(r):
+ x = x*(1-q^(m-i))/(1-q^(i+1));
+
+ return x;
+
+
+#####################################################################
+
+##Gilbert - Varshamov distance of a code over Fq, with length n and dimension k
+##Nw is the number of codewords with weight d (not considering scalar multiples)
+def gv_distance(n,k,q):
+
+ d = 1;
+ right_term = q^(n-k);
+ left_term = 0;
+ while left_term <= right_term:
+ left_term += binomial(n,d)*(q-1)^d;
+ d+=1;
+ d = d-1;
+
+ Nw = binomial(n,d)*(q-1)^(d-2)*q^(k-n+1)*1.;
+
+ return d, Nw;
+
+
+#############################################################
+#Stern's adaptation of ISD over Fq, due to Peters
+#It returns both complexity and optimal parameters
+def peters_isd(n,k,q,w):
+
+ x = floor(k/2);
+
+ log2q=log2(q);
+ mincost=10000000;
+ bestp=0;
+ bestl=0;
+ max_p = min(w//2,floor(k/2));
+ for p in range(0,max_p):
+ Anum=binomial(x,p);
+ Bnum=binomial(k-x,p);
+ #for(l=1,floor(log(Anum)/log(q)+p*log(q-1)/log(q))+10,\
+ for l in range(1,floor( log(Anum)/log(q)+p*log(q-1)/log(q))+10 +1):
+ if n-k-l <= w-2*p:
+ continue
+
+ # if(q==2):
+ ops=0.5*(n-k)^2*(n+k)+ ((0.5*k-p+1)+(Anum+Bnum)*(q-1)^p)*l+ q/(q-1.)*(w-2*p+1)*2*p*(1+(q-2)/(q-1.))*Anum*Bnum*(q-1)^(2*p)/q^l;
+ #ops=(n-k)^2*(n+k)\
+ # + ((0.5*k-p+1)+(Anum+Bnum)*(q-1)^p)*l\
+ # + q/(q-1.)*(w-2*p+1)*2*p*(1+(q-2)/(q-1.))*\
+ # Anum*Bnum*(q-1)^(2*p)/q^l;
+
+ prob=Anum*Bnum*binomial(n-k-l,w-2*p)/binomial(n,w);
+ cost=log2(ops)+log2(log2q)-log2(prob);
+ if cost1:
+ Nw = binomial(n,w)*(q-1)^(w-1)*(q^k-1)/(q^n-1)*1.;
+ if Nw>1:
+ u = round(n*(1-w/n)^Nw);
+ if u>1:
+ w = w+1;
+ else:
+ w = w+1;
+
+ w = w+1;
+ #consider cost of ISD
+ C_isd,p,l = peters_isd(n,k,q,w);
+ cost = C_isd+log2(log(Nw));
+
+ return cost, w, Nw*1.
+
+#############################################################
+
+#Optimize the cost of Beullens' algorithm with improved subcodes finding strategy
+def improved_linear_beullens(n,k,q):
+
+ w_in, Nw = gv_distance(n,k,q);
+
+ max_w = n-k+2;
+
+ best_cost = 100000000000000;
+ best_w = 0;
+ best_L_prime = 0;
+ best_w_prime = 0;
+
+ for w_prime in range(w_in,max_w):
+ # print(w_prime);
+ Nw_prime = binomial(n,w_prime)*(q-1)^(w_prime-1)*(q^k-1)/(q^n-1);
+
+ for w in range(w_prime+1,min(2*w_prime,n)):
+
+ pr_w_w_prime = binomial(w_prime,2*w_prime-w)*binomial(n-w_prime,w-w_prime)/binomial(n,w_prime); #zeta probability in the paper
+
+ L_prime = (2*Nw_prime^2/(pr_w_w_prime)*(2*log(n*1.)))^0.25;
+
+ if L_prime < Nw_prime:
+
+ x = 2*w_prime - w;
+ pw = 0.5*binomial(n,w-w_prime)*binomial(n-(w-w_prime),w-w_prime)*binomial(n-2*(w-w_prime),2*w_prime-w)*factorial(2*w_prime-w)*(q-1)^(w-2*w_prime+1)/(binomial(n,w_prime)*binomial(n-w_prime,w-w_prime)*binomial(w_prime,2*w_prime-w));
+
+ M_second = pr_w_w_prime*L_prime^4/4*pw*(pr_w_w_prime-2/(Nw_prime^2));
+
+
+ if M_second < 1:
+
+ C_isd,p,l = peters_isd(n,k,q,w_prime);
+ #cost = C_isd+log2(2*log(1.-L_prime/Nw_prime)/log(1.-1/Nw_prime)/Nw_prime);
+# cost = C_isd+ log2(L_prime/Nw_prime);
+
+
+
+ if L_prime < Nw_prime/2:
+ cost = C_isd+ log2(L_prime/Nw_prime);
+ else:
+ cost = C_isd+log2(2*log(1.-L_prime/Nw_prime)/log(1.-1./Nw_prime)/Nw_prime);
+
+
+ if cost < best_cost:
+ # print(cost);
+ best_cost = cost;
+ best_w = w;
+ best_w_prime = w_prime;
+ best_L_prime = L_prime;
+
+ return best_cost, best_w, best_w_prime, best_L_prime;
+
+
+#######################################
+
+#Complexity of original Beullen's algorithm to solve linear equivalence
+
+def linear_beullens(n,k,q):
+
+
+ max_w = n-k+2;
+
+ best_cost = 100000000000000;
+ best_w = 0;
+ best_L = 0;
+ best_Nw2 = 0;
+
+ w_in, Nw = gv_distance(n,k,q);
+
+ for w in range(w_in,max_w):
+
+ Nw2 = binomial(n,w)*(q^2-1)^w*gauss_binomial(k,2,q)/((q^2-q)*(q^2-1))/gauss_binomial(n,2,q);
+
+ if Nw2>1:
+
+ L = (Nw2*ceil(n*(n-1)/(2*w*(n-w))))^0.5;
+
+ if L < Nw2:
+
+
+ C_isd,p,l = peters_isd(n,k,q,w);
+
+ #uncomment to consider Prange's ISD
+# C_isd = log2((n^3+binomial(k,2))/(binomial(w,2)*binomial(n-w,k-2)/binomial(n,k)))
+ cost = C_isd+log2(2*log(1.-L/Nw2)/log(1.-1/Nw2)/Nw2);
+ cost = C_isd + log2(L/Nw2);
+
+ if cost < best_cost:
+ # print(cost);
+ best_cost = cost;
+ best_w = w;
+ best_L = L;
+ best_Nw2 = Nw2;
+
+ return best_cost, best_w, best_L, best_Nw2;
+
+
+# Cost of Prange's ISD adaption to find d-dimensional subcodes with support size w
+
+def beullens_isd(q, n, k, d, w, Nw):
+ success_pr = binomial(w, d) * binomial(n - w, k - d) / binomial(n, k);
+ c_iter = k ^ 3 + binomial(k, d);
+
+ #print(c_iter, 1 - (1 - success_pr) ^ Nw)
+ return log2(c_iter) - log2(1 - (1 - success_pr) ^ Nw);
+
+
+# Cost of ISD; depending on d, it considers either Peter's ISD or Belleuns' ISD
+def cost_isd(q, n, k, d, w, Nw):
+ if d == 1:
+
+ # c_isd = peters_isd(q,n,k,w);
+
+ # if c_isd.imag()!=0:
+
+ pr_isd = binomial(w, 1) * binomial(n - w, k - 1) / binomial(n, k);
+ cost = k ^ 3 + binomial(k, 1);
+
+ if (k - 2) < (n - w):
+ pr_isd += binomial(w, 2) * binomial(n - w, k - 2) / binomial(n, k);
+ cost += binomial(k, 2) * (q - 1);
+
+ if pr_isd == 0:
+ c_isd = 10000000000;
+ else:
+ c_isd = log2(cost / (1 - (1 - pr_isd) ^ Nw));
+
+ else:
+
+ c_isd = beullens_isd(q, n, k, d, w, Nw);
+
+ return max(0, c_isd);
+##Testing the complexity on some instances
+
+#n = 200;
+#k = 100;
+#
+#max_q = 256;
+#
+#P = Primes();
+#q_values = [2];
+#q = 2;
+#while q < max_q:
+# q = P.next(q);
+# if q < max_q:
+# q_values.append(q);
+#
+#
+#Leon = [];
+#Beullens = [];
+#improved_Beullens = [];
+#
+#q_values = [11, 17, 53, 103, 151, 199, 251];
+#for q in q_values:
+#
+#
+# print("For q = "+str(q));
+#
+# cost_Leon, w, Nw = leon_cost(n,k,q);
+#
+# print("Leon: cost is "+str(cost_Leon)+", w = "+str(w));
+#
+#
+# improved_cost_Beullens, best_w, best_w_prime, best_L_prime = improved_linear_beullens(n,k,q);
+# Nw_prime = binomial(n,best_w_prime)*(q-1)^(best_w_prime-1)*q^(k-n)*1.;
+#
+# print("Improved Beullens: cost is "+str(improved_cost_Beullens)+", w_prime = "+str(best_w_prime)+", w = "+str(best_w)+", L_prime = "+str(best_L_prime)+", Nw_prime = "+str(Nw_prime));
+#
+#
+# cost_Beullens, best_w, best_L, best_Nw2 = linear_beullens(n,k,q);
+# print("Beullens: cost is "+str(cost_Beullens)+", w = "+str(best_w)+", L = "+str(best_L)+", Nw2 = "+str(best_Nw2*1.));
+#
+#
+#
+# Leon.append((q, cost_Leon));
+# improved_Beullens.append((q,improved_cost_Beullens));
+# Beullens.append((q,cost_Beullens));
+#
+#
+#
+#
+#
+# print("-------------");
diff --git a/tests/module/estimator.py b/tests/module/estimator.py
new file mode 100644
index 00000000..a93549a4
--- /dev/null
+++ b/tests/module/estimator.py
@@ -0,0 +1,1710 @@
+#from .theoretical_estimates import *
+from math import inf, ceil, log2, comb
+from prettytable import PrettyTable
+#from progress.bar import Bar
+from scipy.special import binom as binom_sp
+from scipy.optimize import fsolve
+from warnings import filterwarnings
+
+filterwarnings("ignore", category=RuntimeWarning)
+
+
+def binom(n, k):
+ return comb(int(n), int(k))
+
+
+def __truncate(x, precision):
+ """
+ Truncates a float
+
+ INPUT:
+
+ - ``x`` -- value to be truncated
+ - ``precision`` -- number of decimal places to after which the ``x`` is truncated
+
+ """
+
+ return float(int(x * 10 ** precision) / 10 ** precision)
+
+
+def __concat_pretty_tables(t1, t2):
+ v = t1.split("\n")
+ v2 = t2.split("\n")
+ vnew = ""
+ for i in range(len(v)):
+ vnew += v[i] + v2[i][1:] + "\n"
+ return vnew[:-1]
+
+
+def __round_or_truncate_to_given_precision(T, M, truncate, precision):
+ if truncate:
+ T, M = __truncate(T, precision), __truncate(M, precision)
+ else:
+ T, M = round(T, precision), round(M, precision)
+ return '{:.{p}f}'.format(T, p=precision), '{:.{p}f}'.format(M, p=precision)
+
+
+def __memory_access_cost(mem, memory_access):
+ if memory_access == 0:
+ return 0
+ elif memory_access == 1:
+ return log2(mem)
+ elif memory_access == 2:
+ return mem / 2
+ elif memory_access == 3:
+ return mem / 3
+ elif callable(memory_access):
+ return memory_access(mem)
+ return 0
+
+
+def _gaussian_elimination_complexity(n, k, r):
+ """
+ Complexity estimate of Gaussian elimination routine
+
+ INPUT:
+
+ - ``n`` -- Row additons are perfomed on ``n`` coordinates
+ - ``k`` -- Matrix consists of ``n-k`` rows
+ - ``r`` -- Blocksize of method of the four russian for inversion, default is zero
+
+ [Bar07]_ Bard, G.V.: Algorithms for solving linear and polynomial systems of equations over finite fields
+ with applications to cryptanalysis. Ph.D. thesis (2007)
+
+ [BLP08] Bernstein, D.J., Lange, T., Peters, C.: Attacking and defending the mceliece cryptosystem.
+ In: International Workshop on Post-Quantum Cryptography. pp. 31β46. Springer (2008)
+
+ EXAMPLES::
+
+ >>> from .estimator import _gaussian_elimination_complexity
+ >>> _gaussian_elimination_complexity(n=100,k=20,r=1) # doctest: +SKIP
+
+ """
+
+ if r != 0:
+ return (r ** 2 + 2 ** r + (n - k - r)) * int(((n + r - 1) / r))
+
+ return (n - k) ** 2
+
+
+def _optimize_m4ri(n, k, mem=inf):
+ """
+ Find optimal blocksize for Gaussian elimination via M4RI
+
+ INPUT:
+
+ - ``n`` -- Row additons are perfomed on ``n`` coordinates
+ - ``k`` -- Matrix consists of ``n-k`` rows
+
+ """
+
+ (r, v) = (0, inf)
+ for i in range(n - k):
+ tmp = log2(_gaussian_elimination_complexity(n, k, i))
+ if v > tmp and r < mem:
+ r = i
+ v = tmp
+ return r
+
+
+def _mem_matrix(n, k, r):
+ """
+ Memory usage of parity check matrix in vector space elements
+
+ INPUT:
+
+ - ``n`` -- length of the code
+ - ``k`` -- dimension of the code
+ - ``r`` -- block size of M4RI procedure
+
+ EXAMPLES::
+
+ >>> from .estimator import _mem_matrix
+ >>> _mem_matrix(n=100,k=20,r=0) # doctest: +SKIP
+
+ """
+ return n - k + 2 ** r
+
+
+def _list_merge_complexity(L, l, hmap):
+ """
+ Complexity estimate of merging two lists exact
+
+ INPUT:
+
+ - ``L`` -- size of lists to be merged
+ - ``l`` -- amount of bits used for matching
+ - ``hmap`` -- indicates if hashmap is being used (Default 0: no hashmap)
+
+ EXAMPLES::
+
+ >>> from .estimator import _list_merge_complexity
+ >>> _list_merge_complexity(L=2**16,l=16,hmap=1) # doctest: +SKIP
+
+ """
+
+ if L == 1:
+ return 1
+ if not hmap:
+ return max(1, 2 * int(log2(L)) * L + L ** 2 // 2 ** l)
+ else:
+ return 2 * L + L ** 2 // 2 ** l
+
+
+def _indyk_motwani_complexity(L, l, w, hmap):
+ """
+ Complexity of Indyk-Motwani nearest neighbor search
+
+ INPUT:
+
+ - ``L`` -- size of lists to be matched
+ - ``l`` -- amount of bits used for matching
+ - ``w`` -- target weight
+ - ``hmap`` -- indicates if hashmap is being used (Default 0: no hashmap)
+
+ EXAMPLES::
+
+ >>> from .estimator import _indyk_motwani_complexity
+ >>> _indyk_motwani_complexity(L=2**16,l=16,w=2,hmap=1) # doctest: +SKIP
+
+ """
+
+ if w == 0:
+ return _list_merge_complexity(L, l, hmap)
+ lam = max(0, int(min(ceil(log2(L)), l - 2 * w)))
+ return binom(l, lam) // binom(l - w, lam) * _list_merge_complexity(L, lam, hmap)
+
+
+def _mitm_nn_complexity(L, l, w, hmap):
+ """
+ Complexity of Indyk-Motwani nearest neighbor search
+
+ INPUT:
+
+ - ``L`` -- size of lists to be matched
+ - ``l`` -- amount of bits used for matching
+ - ``w`` -- target weight
+ - ``hmap`` -- indicates if hashmap is being used (Default 0: no hashmap)
+
+ EXAMPLES::
+
+ >>> from .estimator import _indyk_motwani_complexity
+ >>> _indyk_motwani_complexity(L=2**16,l=16,w=2,hmap=1) # doctest: +SKIP
+
+ """
+ if w == 0:
+ return _list_merge_complexity(L, l, hmap)
+ L1 = L * binom(l / 2, w / 2)
+ return _list_merge_complexity(L1, l, hmap)
+
+
+def prange_complexity(n, k, w, mem=inf, memory_access=0):
+ """
+ Complexity estimate of Prange's ISD algorithm
+
+ [Pra62] Prange, E.: The use of information sets in decoding cyclic codes. IRE Transactions
+ on Information Theory 8(5), 5β9 (1962)
+
+ expected weight distribution::
+
+ +--------------------------------+-------------------------------+
+ | <----------+ n - k +---------> | <----------+ k +------------> |
+ | w | 0 |
+ +--------------------------------+-------------------------------+
+
+ INPUT:
+
+ - ``n`` -- length of the code
+ - ``k`` -- dimension of the code
+ - ``w`` -- Hamming weight of error vector
+ - ``mem`` -- upper bound on the available memory (as log2(bits)), default unlimited
+ - ``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::
+
+ >>> from .estimator import prange_complexity
+ >>> prange_complexity(n=100,k=50,w=10) # doctest: +SKIP
+
+ """
+
+ solutions = max(0, log2(binom(n, w)) - (n - k))
+
+ r = _optimize_m4ri(n, k, mem)
+ Tp = max(log2(binom(n, w)) - log2(binom(n - k, w)) - solutions, 0)
+ Tg = log2(_gaussian_elimination_complexity(n, k, r))
+ time = Tp + Tg
+ memory = log2(_mem_matrix(n, k, r))
+
+ time += __memory_access_cost(memory, memory_access)
+
+ params = [r]
+
+ par = {"r": params[0]}
+ res = {"time": time, "memory": memory, "parameters": par}
+ return res
+
+
+def stern_complexity(n, k, w, mem=inf, hmap=1, memory_access=0):
+ """
+ Complexity estimate of Stern's ISD algorithm
+
+ [Ste88] Stern, J.: A method for finding codewords of small weight. In: International
+ Colloquium on Coding Theory and Applications. pp. 106β113. Springer (1988)
+
+ [BLP08] Bernstein, D.J., Lange, T., Peters, C.: Attacking and defending the mceliece cryptosystem.
+ In: International Workshop on Post-Quantum Cryptography. pp. 31β46. Springer (2008)
+
+ expected weight distribution::
+
+ +-------------------------+---------+-------------+-------------+
+ | <----+ n - k - l +----> |<-- l -->|<--+ k/2 +-->|<--+ k/2 +-->|
+ | w - 2p | 0 | p | p |
+ +-------------------------+---------+-------------+-------------+
+
+ INPUT:
+
+ - ``n`` -- length of the code
+ - ``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)
+ - ``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::
+
+ >>> from .estimator import stern_complexity
+ >>> stern_complexity(n=100,k=50,w=10) # doctest: +SKIP
+
+ """
+
+ solutions = max(0, log2(binom(n, w)) - (n - k))
+
+ r = _optimize_m4ri(n, k, mem)
+ time = inf
+ memory = 0
+ params = [-1 for i in range(2)]
+ i_val = [20]
+ i_val_inc = [10]
+ k1 = k // 2
+ while True:
+ stop = True
+ for p in range(min(k1, w // 2, i_val[0])):
+ L1 = binom(k1, p)
+ l_val = int(log2(L1))
+ if log2(L1) > time:
+ continue
+ for l in range(max(l_val - i_val_inc[0], 0), l_val + i_val_inc[0]):
+
+ tmp_mem = log2(2 * L1 + _mem_matrix(n, k, r))
+ if tmp_mem > mem:
+ continue
+
+ Tp = max(0,
+ log2(binom(n, w)) - log2(binom(n - k, w - 2 * p)) - log2(binom(k1, p) ** 2) - solutions)
+
+ # We use Indyk-Motwani (IM) taking into account the possibility of multiple existing solutions
+ # with correct weight distribution, decreasing the amount of necessary projections
+ # remaining_sol denotes the number of expected solutions per permutation
+ # l_part_iterations is the expected number of projections need by IM to find one of those solutions
+
+ remaining_sol = (binom(n - k, w - 2 * p) * binom(k1, p) ** 2 * binom(n, w) // 2 ** (n - k)) // binom(n,
+ w)
+ l_part_iterations = binom(n - k, w - 2 * p) // binom(n - k - l, w - 2 * p)
+
+ if remaining_sol > 0:
+ l_part_iterations //= max(1, remaining_sol)
+ l_part_iterations = max(1, l_part_iterations)
+
+ Tg = _gaussian_elimination_complexity(n, k, r)
+ tmp = Tp + log2(Tg + _list_merge_complexity(L1, l, hmap) * l_part_iterations)
+
+ tmp += __memory_access_cost(tmp_mem, memory_access)
+
+ time = min(time, tmp)
+
+ if tmp == time:
+ memory = tmp_mem
+ params = [p, l]
+
+ for i in range(len(i_val)):
+ if params[i] == i_val[i] - 1:
+ stop = False
+ i_val[i] += i_val_inc[i]
+
+ if stop:
+ break
+
+ par = {"l": params[1], "p": params[0]}
+ res = {"time": time, "memory": memory, "parameters": par}
+ return res
+
+
+def dumer_complexity(n, k, w, mem=inf, hmap=1, memory_access=0):
+ """
+ Complexity estimate of Dumer's ISD algorithm
+
+ [Dum91] Dumer, I.: On minimum distance decoding of linear codes. In: Proc. 5th Joint
+ Soviet-Swedish Int. Workshop Inform. Theory. pp. 50β52 (1991)
+
+ expected weight distribution::
+
+ +--------------------------+------------------+-------------------+
+ | <-----+ n - k - l +----->|<-- (k + l)/2 +-->|<--+ (k + l)/2 +-->|
+ | w - 2p | p | p |
+ +--------------------------+------------------+-------------------+
+
+ INPUT:
+
+ - ``n`` -- length of the code
+ - ``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)
+ - ``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::
+
+ >>> from .estimator import dumer_complexity
+ >>> dumer_complexity(n=100,k=50,w=10) # doctest: +SKIP
+
+
+ """
+ solutions = max(0, log2(binom(n, w)) - (n - k))
+ time = inf
+ memory = 0
+ r = _optimize_m4ri(n, k, mem)
+
+ i_val = [10, 40]
+ i_val_inc = [10, 10]
+ params = [-1 for _ in range(2)]
+ while True:
+ stop = True
+ for p in range(min(w // 2, i_val[0])):
+ for l in range(min(n - k - (w - p), i_val[1])):
+ k1 = (k + l) // 2
+ L1 = binom(k1, p)
+ if log2(L1) > time:
+ continue
+
+ tmp_mem = log2(2 * L1 + _mem_matrix(n, k, r))
+ if tmp_mem > mem:
+ continue
+
+ Tp = max(log2(binom(n, w)) - log2(binom(n - k - l, w - 2 * p)) - log2(binom(k1, p) ** 2) - solutions, 0)
+ Tg = _gaussian_elimination_complexity(n, k, r)
+ tmp = Tp + log2(Tg + _list_merge_complexity(L1, l, hmap))
+
+ tmp += __memory_access_cost(tmp_mem, memory_access)
+
+ time = min(time, tmp)
+ if tmp == time:
+ memory = tmp_mem
+ params = [p, l]
+
+ for i in range(len(i_val)):
+ if params[i] == i_val[i] - 1:
+ stop = False
+ i_val[i] += i_val_inc[i]
+
+ if stop:
+ break
+
+ par = {"l": params[1], "p": params[0]}
+ res = {"time": time, "memory": memory, "parameters": par}
+ return res
+
+
+def ball_collision_decoding_complexity(n, k, w, mem=inf, hmap=1, memory_access=0):
+ """
+ Complexity estimate of the ball collision decodding algorithm
+
+ [BLP11] Bernstein, D.J., Lange, T., Peters, C.: Smaller decoding exponents: ball-collision decoding.
+ In: Annual Cryptology Conference. pp. 743β760. Springer (2011)
+
+ expected weight distribution::
+
+ +------------------+---------+---------+-------------+-------------+
+ | <-+ n - k - l +->|<- l/2 ->|<- l/2 ->|<--+ k/2 +-->|<--+ k/2 +-->|
+ | w - 2p - 2pl | pl | pl | p | p |
+ +------------------+---------+---------+-------------+-------------+
+
+ INPUT:
+
+ - ``n`` -- length of the code
+ - ``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)
+ - ``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::
+
+ >>> from .estimator import ball_collision_decoding_complexity
+ >>> ball_collision_decoding_complexity(n=100,k=50,w=10) # doctest: +SKIP
+
+ """
+ solutions = max(0, log2(binom(n, w)) - (n - k))
+ time = inf
+ memory = 0
+ r = _optimize_m4ri(n, k, mem)
+
+ i_val = [10, 80, 4]
+ i_val_inc = [10, 10, 10]
+ params = [-1 for _ in range(3)]
+ k1 = k // 2
+ while True:
+ stop = True
+ for p in range(min(w // 2, i_val[0])):
+ for l in range(min(n - k - (w - 2 * p), i_val[1])):
+ for pl in range(min(i_val[2], (w - 2 * p) // 2, l // 2 + 1)):
+ L1 = binom(k1, p)
+ L1 *= max(1, binom(l // 2, pl))
+ if log2(L1) > time:
+ continue
+
+ tmp_mem = log2(2 * L1 + _mem_matrix(n, k, r))
+ if tmp_mem > mem:
+ continue
+
+ Tp = max(
+ log2(binom(n, w)) - log2(binom(n - k - l, w - 2 * p - 2 * pl)) - 2 * log2(
+ binom(k1, p)) - 2 * log2(
+ binom(l // 2, pl)) - solutions, 0)
+ Tg = _gaussian_elimination_complexity(n, k, r)
+ tmp = Tp + log2(Tg + _list_merge_complexity(L1, l, hmap))
+
+ tmp += __memory_access_cost(tmp_mem, memory_access)
+
+ time = min(time, tmp)
+ if tmp == time:
+ memory = tmp_mem
+ params = [p, pl, l]
+
+ for i in range(len(i_val)):
+ if params[i] == i_val[i] - 1:
+ stop = False
+ i_val[i] += i_val_inc[i]
+
+ if stop:
+ break
+
+ par = {"l": params[2], "p": params[0], "pl": params[1]}
+ res = {"time": time, "memory": memory, "parameters": par}
+ return res
+
+
+def bjmm_complexity(n, k, w, mem=inf, hmap=1, only_depth_two=0, memory_access=0):
+ """
+ Complexity estimate of BJMM algorithm
+
+ [MMT11] May, A., Meurer, A., Thomae, E.: Decoding random linear codes in 2^(0.054n). In: International Conference
+ on the Theory and Application of Cryptology and Information Security. pp. 107β124. Springer (2011)
+
+ [BJMM12] Becker, A., Joux, A., May, A., Meurer, A.: Decoding random binary linear codes in 2^(n/20): How 1+ 1= 0
+ improves information set decoding. In: Annual international conference on the theory and applications of
+ cryptographic techniques. pp. 520β536. Springer (2012)
+
+ expected weight distribution::
+
+ +--------------------------+-------------------+-------------------+
+ | <-----+ n - k - l +----->|<--+ (k + l)/2 +-->|<--+ (k + l)/2 +-->|
+ | w - 2p | p | p |
+ +--------------------------+-------------------+-------------------+
+
+ INPUT:
+
+ - ``n`` -- length of the code
+ - ``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)
+ - ``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::
+
+ >>> from .estimator import bjmm_complexity
+ >>> bjmm_complexity(n=100,k=50,w=10) # doctest: +SKIP
+
+ """
+ d2 = bjmm_depth_2_complexity(n, k, w, mem, hmap, memory_access)
+ d3 = bjmm_depth_3_complexity(n, k, w, mem, hmap, memory_access)
+ return d2 if d2["time"] < d3["time"] or only_depth_two else d3
+
+
+def bjmm_depth_2_complexity(n, k, w, mem=inf, hmap=1, memory_access=0, mmt=0):
+ """
+ Complexity estimate of BJMM algorithm in depth 2
+
+ [MMT11] May, A., Meurer, A., Thomae, E.: Decoding random linear codes in 2^(0.054n). In: International Conference
+ on the Theory and Application of Cryptology and Information Security. pp. 107β124. Springer (2011)
+
+ [BJMM12] Becker, A., Joux, A., May, A., Meurer, A.: Decoding random binary linear codes in 2^(n/20): How 1+ 1= 0
+ improves information set decoding. In: Annual international conference on the theory and applications of
+ cryptographic techniques. pp. 520β536. Springer (2012)
+
+ expected weight distribution::
+
+ +--------------------------+-------------------+-------------------+
+ | <-----+ n - k - l +----->|<--+ (k + l)/2 +-->|<--+ (k + l)/2 +-->|
+ | w - 2p | p | p |
+ +--------------------------+-------------------+-------------------+
+
+ INPUT:
+
+ - ``n`` -- length of the code
+ - ``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)
+ - ``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)
+ - ``mmt`` -- restrict optimization to use of MMT algorithm (precisely enforce p1=p/2)
+
+ EXAMPLES::
+
+ >>> from .estimator import bjmm_depth_2_complexity
+ >>> bjmm_depth_2_complexity(n=100,k=50,w=10) # doctest: +SKIP
+
+ """
+ solutions = max(0, log2(binom(n, w)) - (n - k))
+ time = inf
+ memory = 0
+ r = _optimize_m4ri(n, k, mem)
+
+ i_val = [35, 500, 35]
+ i_val_inc = [10, 10, 10]
+ params = [-1 for _ in range(3)]
+ while True:
+ stop = True
+ for p in range(max(params[0] - i_val_inc[0] // 2, 0), min(w // 2, i_val[0]), 2):
+ for l in range(max(params[1] - i_val_inc[1] // 2, 0), min(n - k - (w - 2 * p), min(i_val[1], n - k))):
+ for p1 in range(max(params[2] - i_val_inc[2] // 2, (p + 1) // 2), min(w, i_val[2])):
+ if mmt and p1 != p // 2:
+ continue
+ k1 = (k + l) // 2
+ L1 = binom(k1, p1)
+ if log2(L1) > time:
+ continue
+
+ if k1 - p < p1 - p / 2:
+ continue
+ reps = (binom(p, p / 2) * binom(k1 - p, p1 - p / 2)) ** 2
+
+ l1 = int(ceil(log2(reps)))
+
+ if l1 > l:
+ continue
+
+ L12 = max(1, L1 ** 2 // 2 ** l1)
+
+ tmp_mem = log2((2 * L1 + L12) + _mem_matrix(n, k, r))
+ if tmp_mem > mem:
+ continue
+
+ Tp = max(log2(binom(n, w)) - log2(binom(n - k - l, w - 2 * p)) - 2 * log2(
+ binom((k + l) // 2, p)) - solutions, 0)
+ Tg = _gaussian_elimination_complexity(n, k, r)
+ T_tree = 2 * _list_merge_complexity(L1, l1, hmap) + _list_merge_complexity(L12,
+ l - l1,
+ hmap)
+ T_rep = int(ceil(2 ** (l1 - log2(reps))))
+
+ tmp = Tp + log2(Tg + T_rep * T_tree)
+ tmp += __memory_access_cost(tmp_mem, memory_access)
+
+ time = min(tmp, time)
+ if tmp == time:
+ memory = tmp_mem
+ params = [p, l, p1]
+
+ for i in range(len(i_val)):
+ if params[i] == i_val[i] - 1:
+ stop = False
+ i_val[i] += i_val_inc[i]
+
+ if stop:
+ break
+
+ par = {"l": params[1], "p": params[0], "p1": params[2], "depth": 2}
+ res = {"time": time, "memory": memory, "parameters": par}
+ return res
+
+
+def bjmm_depth_3_complexity(n, k, w, mem=inf, hmap=1, memory_access=0):
+ """
+ Complexity estimate of BJMM algorithm in depth 3
+
+ [MMT11] May, A., Meurer, A., Thomae, E.: Decoding random linear codes in 2^(0.054n). In: International Conference
+ on the Theory and Application of Cryptology and Information Security. pp. 107β124. Springer (2011)
+
+ [BJMM12] Becker, A., Joux, A., May, A., Meurer, A.: Decoding random binary linear codes in 2^(n/20): How 1+ 1= 0
+ improves information set decoding. In: Annual international conference on the theory and applications of
+ cryptographic techniques. pp. 520β536. Springer (2012)
+
+ expected weight distribution::
+
+ +--------------------------+-------------------+-------------------+
+ | <-----+ n - k - l +----->|<--+ (k + l)/2 +-->|<--+ (k + l)/2 +-->|
+ | w - 2p | p | p |
+ +--------------------------+-------------------+-------------------+
+
+ INPUT:
+
+ - ``n`` -- length of the code
+ - ``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)
+ - ``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::
+
+ >>> from .estimator import bjmm_depth_3_complexity
+ >>> bjmm_depth_3_complexity(n=100,k=50,w=10) # doctest: +SKIP
+
+ """
+ solutions = max(0, log2(binom(n, w)) - (n - k))
+ time = inf
+ memory = 0
+ r = _optimize_m4ri(n, k, mem)
+
+ params = [-1 for _ in range(4)]
+ i_val = [25, 400, 20, 10]
+ i_val_inc = [10, 10, 10, 10]
+ while True:
+ stop = True
+ for p in range(max(params[0] - i_val_inc[0] // 2 + (params[0] - i_val_inc[0] // 2) % 2, 0),
+ min(w // 2, i_val[0]), 2):
+ for l in range(max(params[1] - i_val_inc[1] // 2, 0), min(n - k - (w - 2 * p), min(n - k, i_val[1]))):
+ k1 = (k + l) // 2
+ for p2 in range(max(params[2] - i_val_inc[2] // 2, p // 2 + ((p // 2) % 2)), i_val[2], 2):
+ for p1 in range(max(params[3] - i_val_inc[3] // 2, (p2 + 1) // 2), i_val[3]):
+ L1 = binom(k1, p1)
+
+ if log2(L1) > time:
+ continue
+
+ reps1 = (binom(p2, p2 / 2) * binom(k1 - p2, p1 - p2 / 2)) ** 2
+ l1 = int((log2(reps1))) if reps1 != 1 else 0
+
+ L12 = max(1, L1 ** 2 // 2 ** l1)
+ reps2 = (binom(p, p / 2) * binom(k1 - p, p2 - p / 2)) ** 2
+ l2 = int(ceil(log2(reps2))) if reps2 != 1 else 0
+
+ L1234 = max(1, L12 ** 2 // 2 ** (l2 - l1))
+ tmp_mem = log2((2 * L1 + L12 + L1234) + _mem_matrix(n, k, r))
+ if tmp_mem > mem:
+ continue
+
+ Tp = max(log2(binom(n, w)) - log2(binom(n - k - l, w - 2 * p)) - 2 * log2(
+ binom((k + l) // 2, p)) - solutions, 0)
+ Tg = _gaussian_elimination_complexity(n, k, r)
+ T_tree = 4 * _list_merge_complexity(L1, l1, hmap) + 2 * _list_merge_complexity(L12,
+ l2 - l1,
+ hmap) + _list_merge_complexity(
+ L1234,
+ l - l2,
+ hmap)
+ T_rep = int(ceil(2 ** (3 * max(0, l1 - log2(reps1)) + max(0, l2 - log2(reps2)))))
+
+ tmp = Tp + log2(Tg + T_rep * T_tree)
+ tmp += __memory_access_cost(tmp_mem, memory_access)
+
+ if tmp < time:
+ time = tmp
+ memory = tmp_mem
+ params = [p, l, p2, p1]
+
+ for i in range(len(i_val)):
+ if params[i] >= i_val[i] - i_val_inc[i] / 2:
+ stop = False
+ i_val[i] += i_val_inc[i]
+
+ if stop:
+ break
+
+ par = {"l": params[1], "p": params[0], "p1": params[3], "p2": params[2], "depth": 3}
+ res = {"time": time, "memory": memory, "parameters": par}
+ return res
+
+
+def bjmm_depth_2_partially_disjoint_weight_complexity(n, k, w, mem=inf, hmap=1, memory_access=0):
+ """
+ Complexity estimate of BJMM algorithm in depth 2 using partially disjoint weight, applying explicit MitM-NN search on second level
+
+ [MMT11] May, A., Meurer, A., Thomae, E.: Decoding random linear codes in 2^(0.054n). In: International Conference
+ on the Theory and Application of Cryptology and Information Security. pp. 107β124. Springer (2011)
+
+ [BJMM12] Becker, A., Joux, A., May, A., Meurer, A.: Decoding random binary linear codes in 2^(n/20): How 1+ 1= 0
+ improves information set decoding. In: Annual international conference on the theory and applications of
+ cryptographic techniques. pp. 520β536. Springer (2012)
+
+ [EssBel21] Esser, A. and Bellini, E.: Syndrome Decoding Estimator. In: IACR Cryptol. ePrint Arch. 2021 (2021), 1243
+
+ 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:
+
+ - ``n`` -- length of the code
+ - ``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)
+ - ``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::
+
+ >>> from .estimator import bjmm_depth_2_partially_disjoint_weight_complexity
+ >>> bjmm_depth_2_partially_disjoint_weight_complexity(n=100,k=50,w=10) # doctest: +SKIP
+
+ """
+ solutions = max(0, log2(binom(n, w)) - (n - k))
+ time = inf
+ memory = 0
+ r = _optimize_m4ri(n, k, mem)
+
+ i_val = [30, 25, 5]
+ i_val_inc = [10, 10, 10, 10, 10]
+ params = [-1 for _ in range(5)]
+ while True:
+ stop = True
+ for p in range(max(params[0] - i_val_inc[0] // 2, 0), min(w // 2, i_val[0]), 2):
+ for p1 in range(max(params[1] - i_val_inc[1] // 2, (p + 1) // 2), min(w, i_val[1])):
+ for w2 in range(max(params[2] - i_val_inc[2] // 2, 0), min(w - p1, i_val[2])):
+
+ #############################################################################################
+ ######choose start value for l1 close to the logarithm of the number of representations######
+ #############################################################################################
+ try:
+ f = lambda x: log2((binom(p, p // 2) * binom_sp((k + x) / 2 - p, p1 - p // 2))) * 2 - x
+ l1_val = int(fsolve(f, 0)[0])
+ except:
+ continue
+ if f(l1_val) < 0 or f(l1_val) > 1:
+ continue
+ #############################################################################################
+
+ for l1 in range(max(0, l1_val - i_val_inc[3] // 2), l1_val + i_val_inc[3] // 2):
+ k1 = (k + l1) // 2
+ reps = (binom(p, p // 2) * binom(k1 - p, p1 - p // 2)) ** 2
+
+ L1 = binom(k1, p1)
+ if log2(L1) > time:
+ continue
+
+ L12 = L1 ** 2 // 2 ** l1
+ L12 = max(L12, 1)
+ tmp_mem = log2((2 * L1 + L12) + _mem_matrix(n, k, r))
+ if tmp_mem > mem:
+ continue
+
+ #################################################################################
+ #######choose start value for l2 such that resultlist size is close to L12#######
+ #################################################################################
+ try:
+ f = lambda x: log2(int(L12)) + int(2) * log2(binom_sp(x, int(w2))) - int(2) * x
+ l2_val = int(fsolve(f, 0)[0])
+ except:
+ continue
+ if f(l2_val) < 0 or f(l2_val) > 1:
+ continue
+ ################################################################################
+ l2_min = w2
+ l2_max = (n - k - l1 - (w - 2 * p - 2 * w2)) // 2
+ l2_range = [l2_val - i_val_inc[4] // 2, l2_val + i_val_inc[4] // 2]
+ 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 * p - 2 * w2)) - 2 * log2(
+ binom(k1, p)) - 2 * log2(binom(l2, w2)) - solutions, 0)
+ Tg = _gaussian_elimination_complexity(n, k, r)
+
+ T_tree = 2 * _list_merge_complexity(L1, l1, hmap) + _mitm_nn_complexity(L12, 2 * l2, 2 * w2,
+ hmap)
+ T_rep = int(ceil(2 ** max(l1 - log2(reps), 0)))
+
+ tmp = Tp + log2(Tg + T_rep * T_tree)
+ tmp += __memory_access_cost(tmp_mem, memory_access)
+
+ time = min(tmp, time)
+
+ if tmp == time:
+ memory = tmp_mem
+ params = [p, p1, w2, l2, l1]
+
+ for i in range(len(i_val)):
+ if params[i] >= i_val[i] - i_val_inc[i] / 2:
+ i_val[i] += i_val_inc[i]
+ stop = False
+ if stop:
+ break
+ break
+
+ par = {"l1": params[4], "p": params[0], "p1": params[1], "depth": 2, "l2": params[3], "w2": params[2]}
+ res = {"time": time, "memory": memory, "parameters": par}
+ return res
+
+
+def bjmm_depth_2_disjoint_weight_complexity(n, k, w, mem=inf, hmap=1, p_range=[0, 25], memory_access=0):
+ """
+ Complexity estimate of May-Ozerov algorithm in depth 2 using Indyk-Motwani for NN search
+
+
+ [MMT11] May, A., Meurer, A., Thomae, E.: Decoding random linear codes in 2^(0.054n). In: International Conference
+ on the Theory and Application of Cryptology and Information Security. pp. 107β124. Springer (2011)
+
+ [BJMM12] Becker, A., Joux, A., May, A., Meurer, A.: Decoding random binary linear codes in 2^(n/20): How 1+ 1= 0
+ improves information set decoding. In: Annual international conference on the theory and applications of
+ cryptographic techniques. pp. 520β536. Springer (2012)
+
+ [EssBel21] Esser, A. and Bellini, E.: Syndrome Decoding Estimator. In: IACR Cryptol. ePrint Arch. 2021 (2021), 1243
+
+ 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:
+
+ - ``n`` -- length of the code
+ - ``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::
+
+ >>> from .estimator import bjmm_depth_2_disjoint_weight_complexity
+ >>> bjmm_depth_2_disjoint_weight_complexity(n=100,k=50,w=10) # doctest: +SKIP
+
+ """
+
+ solutions = max(0, log2(binom(n, w)) - (n - k))
+ time = inf
+ memory = 0
+ r = _optimize_m4ri(n, k)
+ i_val = [p_range[1], 20, 10, 10, 5]
+ i_val_inc = [10, 10, 10, 10, 10, 10, 10]
+ params = [-1 for _ in range(7)]
+ while True:
+ stop = True
+ for p in range(max(p_range[0], params[0] - i_val_inc[0] // 2, 0), min(w // 2, i_val[0]), 2):
+ for p1 in range(max(params[1] - i_val_inc[1] // 2, (p + 1) // 2), min(w, i_val[1])):
+ s = max(params[2] - i_val_inc[2] // 2, 0)
+ for w1 in range(s - (s % 2), min(w // 2 - p, i_val[2]), 2):
+ for w11 in range(max(params[3] - i_val_inc[3] // 2, (w1 + 1) // 2), min(w, i_val[3])):
+ for w2 in range(max(params[4] - i_val_inc[4] // 2, 0), min(w // 2 - p - w1, i_val[4])):
+ ##################################################################################
+ ######choose start value for l1 such that representations cancel out exactly######
+ ##################################################################################
+ try:
+ f = lambda x: 2 * log2((binom(p, p // 2) * binom(k // 2 - p, p1 - p // 2)) * (
+ binom_sp(x, w1 // 2) * binom_sp(x - w1, w11 - w1 // 2)) + 1) - 2 * x
+ l1_val = int(
+ fsolve(f, 2 * log2((binom(p, p // 2) * binom(k // 2 - p, p1 - p // 2))))[0])
+ except:
+ continue
+ if f(l1_val) < 0 or f(l1_val) > 10:
+ continue
+ #################################################################################
+
+ for l1 in range(max(l1_val - i_val_inc[5], w1, w11), l1_val + i_val_inc[5]):
+ k1 = k // 2
+ reps = (binom(p, p // 2) * binom(k1 - p, p1 - p // 2)) ** 2 * (
+ binom(w1, w1 // 2) * binom(l1 - w1, w11 - w1 // 2)) ** 2
+ reps = max(reps, 1)
+ L1 = binom(k1, p1)
+ if log2(L1) > time:
+ continue
+
+ L12 = L1 ** 2 * binom(l1, w11) ** 2 // 2 ** (2 * l1)
+ L12 = max(L12, 1)
+ tmp_mem = log2((2 * L1 + L12) + _mem_matrix(n, k, r))
+ if tmp_mem > mem:
+ continue
+
+ #################################################################################
+ #######choose start value for l2 such that resultlist size is equal to L12#######
+ #################################################################################
+ try:
+ f = lambda x: log2(L12) + 2 * log2(binom_sp(x, w2) + 1) - 2 * x
+ l2_val = int(fsolve(f, 50)[0])
+ except:
+ continue
+ if f(l2_val) < 0 or f(l2_val) > 10:
+ continue
+ ################################################################################
+ l2_max = (n - k - 2 * l1 - (w - 2 * p - 2 * w1 - 2 * w2)) // 2
+ l2_min = w2
+ l2_range = [l2_val - i_val_inc[6] // 2, l2_val + i_val_inc[6] // 2]
+ 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 - 2 * l1 - 2 * l2, w - 2 * p - 2 * w1 - 2 * w2)) - 2 * log2(
+ binom(k1, p)) - 2 * log2(binom(l1, w1)) - 2 * log2(
+ binom(l2, w2)) - solutions, 0)
+ Tg = _gaussian_elimination_complexity(n, k, r)
+
+ T_tree = 2 * _mitm_nn_complexity(L1, 2 * l1, 2 * w11, hmap) + _mitm_nn_complexity(
+ L12, 2 * l2, 2 * w2, hmap)
+ T_rep = int(ceil(2 ** max(2 * l1 - log2(reps), 0)))
+
+ tmp = Tp + log2(Tg + T_rep * T_tree)
+ tmp += __memory_access_cost(tmp_mem, memory_access)
+
+ time = min(tmp, time)
+
+ if tmp == time:
+ memory = tmp_mem
+ params = [p, p1, w1, w11, w2, l2, l1 + l2]
+
+ for i in range(len(i_val)):
+ if params[i] >= i_val[i] - i_val_inc[i] / 2:
+ i_val[i] += i_val_inc[i]
+ stop = False
+ if stop:
+ break
+ break
+ par = {"l": params[6], "p": params[0], "p1": params[1], "w1": params[2], "w11": params[3], "l2": params[5],
+ "w2": params[4], "depth": 2}
+ res = {"time": time, "memory": memory, "parameters": par}
+ return res
+
+
+def both_may_depth_2_complexity(n, k, w, mem=inf, hmap=1, memory_access=0):
+ """
+ Complexity estimate of Both-May algorithm in depth 2 using Indyk-Motwani and MitM for NN search
+
+ [BotMay18] Both, L., May, A.: Decoding linear codes with high error rate and its impact for LPN security. In:
+ International Conference on Post-Quantum Cryptography. pp. 25--46. Springer (2018)
+
+ expected weight distribution::
+
+ +-------------------+---------+-------------------+-------------------+
+ | <--+ n - k - l+-->|<-+ l +->|<----+ k / 2 +---->|<----+ k / 2 +---->|
+ | w - w2 - 2p | w2 | p | p |
+ +-------------------+---------+-------------------+-------------------+
+
+ INPUT:
+
+ - ``n`` -- length of the code
+ - ``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)
+ - ``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::
+
+ >>> from .estimator import both_may_depth_2_complexity
+ >>> both_may_depth_2_complexity(n=100,k=50,w=10) # doctest: +SKIP
+
+ """
+
+ solutions = max(0, log2(binom(n, w)) - (n - k))
+ time = inf
+ memory = 0
+ r = _optimize_m4ri(n, k, mem)
+
+ i_val = [20, 160, 5, 4, 15]
+ i_val_inc = [10, 10, 10, 6, 6]
+ params = [-1 for _ in range(5)]
+ while True:
+ stop = True
+ for p in range(max(params[0] - i_val_inc[0] // 2, 0), min(w // 2, i_val[0]), 2):
+ for l in range(max(params[1] - i_val_inc[1] // 2, 0), min(n - k - (w - 2 * p), i_val[1])):
+ for w1 in range(max(params[2] - i_val_inc[2] // 2, 0), min(w, l + 1, i_val[2])):
+ for w2 in range(max(params[3] - i_val_inc[3] // 2, 0), min(w - 2 * p, l + 1, i_val[3], 2 * w1), 2):
+ for p1 in range(max(params[4] - i_val_inc[4] // 2, (p + 1) // 2), min(w, i_val[4])):
+ k1 = (k) // 2
+ reps = (binom(p, p / 2) * binom(k1 - p, p1 - p / 2)) ** 2 * binom(w2, w2 / 2) * binom(
+ l - w2,
+ w1 - w2 / 2)
+ reps = 1 if reps == 0 else reps
+ L1 = binom(k1, p1)
+
+ if log2(L1) > time:
+ continue
+
+ L12 = max(1, L1 ** 2 * binom(l, w1) // 2 ** l)
+
+ tmp_mem = log2((2 * L1 + L12) + _mem_matrix(n, k, r))
+ if tmp_mem > mem:
+ continue
+ Tp = max(log2(binom(n, w)) - log2(binom(n - k - l, w - w2 - 2 * p)) - 2 * log2(
+ binom(k1, p)) - log2(binom(l, w2)) - solutions, 0)
+ Tg = _gaussian_elimination_complexity(n, k, r)
+
+ first_level_nn = _indyk_motwani_complexity(L1, l, w1, hmap)
+ second_level_nn = _indyk_motwani_complexity(L12, n - k - l, w - 2 * p - w2, hmap)
+ T_tree = 2 * first_level_nn + second_level_nn
+ T_rep = int(ceil(2 ** max(0, l - log2(reps))))
+
+ tmp = Tp + log2(Tg + T_rep * T_tree)
+ tmp += __memory_access_cost(tmp_mem, memory_access)
+
+ time = min(tmp, time)
+
+ if tmp == time:
+ memory = tmp_mem
+ params = [p, l, w1, w2, p1, log2(L1), log2(L12)]
+
+ for i in range(len(i_val)):
+ if params[i] >= i_val[i] - i_val_inc[i] / 2:
+ i_val[i] += i_val_inc[i]
+ stop = False
+ if stop:
+ break
+
+ par = {"l": params[1], "p": params[0], "p1": params[4], "w1": params[2], "w2": params[3], "depth": 2}
+ res = {"time": time, "memory": memory, "parameters": par}
+ return res
+
+
+def may_ozerov_complexity(n, k, w, mem=inf, hmap=1, only_depth_two=0, memory_access=0):
+ """
+ Complexity estimate of May-Ozerov algorithm using Indyk-Motwani for NN search
+
+ [MayOze15] May, A. and Ozerov, I.: On computing nearest neighbors with applications to decoding of binary linear codes.
+ In: Annual International Conference on the Theory and Applications of Cryptographic Techniques. pp. 203--228. Springer (2015)
+
+ expected weight distribution::
+
+ +-------------------------+---------------------+---------------------+
+ | <-----+ n - k - l+----->|<--+ (k + l) / 2 +-->|<--+ (k + l) / 2 +-->|
+ | w - 2p | p | p |
+ +-------------------------+---------------------+---------------------+
+
+
+ INPUT:
+
+ - ``n`` -- length of the code
+ - ``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)
+ - ``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::
+
+ >>> from .estimator import may_ozerov_complexity
+ >>> may_ozerov_complexity(n=100,k=50,w=10) # doctest: +SKIP
+
+ """
+ d2 = may_ozerov_depth_2_complexity(n, k, w, mem, hmap, memory_access)
+ d3 = may_ozerov_depth_3_complexity(n, k, w, mem, hmap, memory_access)
+ return d2 if d2["time"] < d3["time"] or only_depth_two else d3
+
+
+def may_ozerov_depth_2_complexity(n, k, w, mem=inf, hmap=1, memory_access=0):
+ """
+ Complexity estimate of May-Ozerov algorithm in depth 2 using Indyk-Motwani for NN search
+
+ [MayOze15] May, A. and Ozerov, I.: On computing nearest neighbors with applications to decoding of binary linear codes.
+ In: Annual International Conference on the Theory and Applications of Cryptographic Techniques. pp. 203--228. Springer (2015)
+
+ expected weight distribution::
+
+ +-------------------------+---------------------+---------------------+
+ | <-----+ n - k - l+----->|<--+ (k + l) / 2 +-->|<--+ (k + l) / 2 +-->|
+ | w - 2p | p | p |
+ +-------------------------+---------------------+---------------------+
+
+
+ INPUT:
+
+ - ``n`` -- length of the code
+ - ``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)
+ - ``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::
+
+ >>> from .estimator import may_ozerov_depth_2_complexity
+ >>> may_ozerov_depth_2_complexity(n=100,k=50,w=10) # doctest: +SKIP
+
+ """
+ solutions = max(0, log2(binom(n, w)) - (n - k))
+ time = inf
+ memory = 0
+ r = _optimize_m4ri(n, k, mem)
+
+ i_val = [30, 300, 25]
+ i_val_inc = [10, 10, 10]
+ params = [-1 for _ in range(3)]
+ while True:
+ stop = True
+ for p in range(max(params[0] - i_val_inc[0] // 2, 0), min(w // 2, i_val[0]), 2):
+ for l in range(max(params[1] - i_val_inc[1] // 2, 0), min(n - k - (w - 2 * p), i_val[1])):
+ for p1 in range(max(params[2] - i_val_inc[2] // 2, (p + 1) // 2), min(w, i_val[2])):
+ k1 = (k + l) // 2
+ reps = (binom(p, p // 2) * binom(k1 - p, p1 - p // 2)) ** 2
+
+ L1 = binom(k1, p1)
+ if log2(L1) > time:
+ continue
+
+ L12 = L1 ** 2 // 2 ** l
+ L12 = max(L12, 1)
+ tmp_mem = log2((2 * L1 + L12) + _mem_matrix(n, k, r))
+ if tmp_mem > mem:
+ continue
+
+ Tp = max(
+ log2(binom(n, w)) - log2(binom(n - k - l, w - 2 * p)) - 2 * log2(binom(k1, p)) - solutions, 0)
+ Tg = _gaussian_elimination_complexity(n, k, r)
+
+ T_tree = 2 * _list_merge_complexity(L1, l, hmap) + _indyk_motwani_complexity(L12,
+ n - k - l,
+ w - 2 * p,
+ hmap)
+ T_rep = int(ceil(2 ** max(l - log2(reps), 0)))
+
+ tmp = Tp + log2(Tg + T_rep * T_tree)
+ tmp += __memory_access_cost(tmp_mem, memory_access)
+
+ time = min(tmp, time)
+
+ if tmp == time:
+ memory = tmp_mem
+ params = [p, l, p1]
+
+ for i in range(len(i_val)):
+ if params[i] >= i_val[i] - i_val_inc[i] / 2:
+ i_val[i] += i_val_inc[i]
+ stop = False
+ if stop:
+ break
+ break
+
+ par = {"l": params[1], "p": params[0], "p1": params[2], "depth": 2}
+ res = {"time": time, "memory": memory, "parameters": par}
+ return res
+
+
+def may_ozerov_depth_3_complexity(n, k, w, mem=inf, hmap=1, memory_access=0):
+ """
+ Complexity estimate of May-Ozerov algorithm in depth 3 using Indyk-Motwani for NN search
+
+ [MayOze15] May, A. and Ozerov, I.: On computing nearest neighbors with applications to decoding of binary linear codes.
+ In: Annual International Conference on the Theory and Applications of Cryptographic Techniques. pp. 203--228. Springer (2015)
+
+ expected weight distribution::
+
+ +-------------------------+---------------------+---------------------+
+ | <-----+ n - k - l+----->|<--+ (k + l) / 2 +-->|<--+ (k + l) / 2 +-->|
+ | w - 2p | p | p |
+ +-------------------------+---------------------+---------------------+
+
+ INPUT:
+
+ - ``n`` -- length of the code
+ - ``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)
+ - ``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::
+
+ >>> from .estimator import may_ozerov_depth_3_complexity
+ >>> may_ozerov_depth_3_complexity(n=100,k=50,w=10) # doctest: +SKIP
+
+ """
+ solutions = max(0, log2(binom(n, w)) - (n - k))
+ time = inf
+ memory = 0
+ r = _optimize_m4ri(n, k, mem)
+
+ i_val = [20, 200, 20, 10]
+ i_val_inc = [10, 10, 10, 10]
+ params = [-1 for _ in range(4)]
+ while True:
+ stop = True
+ for p in range(max(params[0] - i_val_inc[0] // 2, 0), min(w // 2, i_val[0]), 2):
+ for l in range(max(params[1] - i_val_inc[1] // 2, 0), min(n - k - (w - 2 * p), i_val[1])):
+ k1 = (k + l) // 2
+ for p2 in range(max(params[2] - i_val_inc[2] // 2, p // 2 + ((p // 2) % 2)), p + i_val[2], 2):
+ for p1 in range(max(params[3] - i_val_inc[3] // 2, (p2 + 1) // 2),
+ min(p2 + i_val[3], k1 - p2 // 2)):
+ L1 = binom(k1, p1)
+ if log2(L1) > time:
+ continue
+
+ reps1 = (binom(p2, p2 // 2) * binom(k1 - p2, p1 - p2 // 2)) ** 2
+ l1 = int(ceil(log2(reps1)))
+
+ if l1 > l:
+ continue
+ L12 = max(1, L1 ** 2 // 2 ** l1)
+ reps2 = (binom(p, p // 2) * binom(k1 - p, p2 - p // 2)) ** 2
+
+ L1234 = max(1, L12 ** 2 // 2 ** (l - l1))
+ tmp_mem = log2((2 * L1 + L12 + L1234) + _mem_matrix(n, k, r))
+ if tmp_mem > mem:
+ continue
+
+ Tp = max(
+ log2(binom(n, w)) - log2(binom(n - k - l, w - 2 * p)) - 2 * log2(binom(k1, p)) - solutions,
+ 0)
+ Tg = _gaussian_elimination_complexity(n, k, r)
+ T_tree = 4 * _list_merge_complexity(L1, l1, hmap) + 2 * _list_merge_complexity(L12,
+ l - l1,
+ hmap) + _indyk_motwani_complexity(
+ L1234,
+ n - k - l,
+ w - 2 * p,
+ hmap)
+ T_rep = int(ceil(2 ** (max(l - log2(reps2), 0) + 3 * max(l1 - log2(reps1), 0))))
+ tmp = Tp + log2(Tg + T_rep * T_tree)
+ tmp += __memory_access_cost(tmp_mem, memory_access)
+
+ if tmp < time:
+ time = tmp
+ memory = tmp_mem
+ params = [p, l, p2, p1]
+ for i in range(len(i_val)):
+ if params[i] >= i_val[i] - i_val_inc[i] / 2:
+ i_val[i] += i_val_inc[i]
+ stop = False
+ if stop:
+ break
+ break
+ par = {"l": params[1], "p": params[0], "p1": params[3], "p2": params[2], "depth": 3}
+ res = {"time": time, "memory": memory, "parameters": par}
+
+ return res
+
+
+def quantum_prange_complexity(n, k, w, maxdepth=96, matrix_mult_constant=2.5):
+ """
+ Optimistic complexity estimate of quantum version of Prange's algorithm
+
+ [Pra62] Prange, E.: The use of information sets in decoding cyclic codes. IRE Transactions
+ on Information Theory 8(5), 5β9 (1962)
+
+ [Ber10] Bernstein, D.J.: Grover vs. McEliece. In: International Workshop on Post-QuantumCryptography.
+ pp. 73β80. Springer (2010)
+
+ expected weight distribution::
+
+ +--------------------------------+-------------------------------+
+ | <----------+ n - k +---------> | <----------+ k +------------> |
+ | w | 0 |
+ +--------------------------------+-------------------------------+
+
+ INPUT:
+
+ - ``n`` -- length of the code
+ - ``k`` -- dimension of the code
+ - ``w`` -- Hamming weight of error vector
+ - ``maxdepth`` -- maximum allowed depth of the quantum circuit (default: 96)
+ - ``matrix_mult_constant`` -- used matrix multiplication constant (default: 2.5)
+
+
+ EXAMPLES::
+
+ >>> from .estimator import quantum_prange_complexity
+ >>> quantum_prange_complexity(n=100,k=50,w=10) # doctest: +SKIP
+
+ """
+
+ Tg = matrix_mult_constant * log2(n - k)
+ if Tg > maxdepth:
+ return 0
+
+ full_circuit = Tg + (log2(binom(n, w)) - log2(binom(n - k, w))) / 2
+ if full_circuit < maxdepth:
+ return full_circuit
+
+ time = log2(binom(n, w)) - log2(binom(n - k, w)) + 2 * Tg - maxdepth
+ return time
+
+
+def sd_estimate_display(n, k, w, memory_limit=inf, bit_complexities=1, hmap=1, skip=["BJMM-dw"], precision=1,
+ truncate=0,
+ all_parameters=0, theoretical_estimates=0, use_mo=1, workfactor_accuracy=1, limit_depth=0,
+ quantum_estimates=1,
+ maxdepth=96, matrix_mult_constant=2.5, memory_access=0):
+ """
+ Output estimates of complexity to solve the syndrome decoding problem
+
+ INPUT:
+
+ - ``n`` -- length of the code
+ - ``k`` -- dimension of the code
+ - ``w`` -- Hamming weight of error vector
+ - ``memory_limit`` -- upper bound on the available memory (in log2) (default: unlimited)
+ - ``bit_complexities`` -- state security level in number of bitoperations, otherwise field operations (default: true)
+ - ``hmap`` -- indicates if hashmap is used for sorting lists (default: true)
+ - ``skip`` -- list of algorithms not to consider (default: ["BJMM-dw"] (this variant will take a long time to optimize))
+ - ``precision`` -- amount of decimal places displayed for complexity estimates (default: 1)
+ - ``truncate`` -- decimal places exceeding ``precision`` are truncated, otherwise rounded (default: false)
+ - ``all_parameters`` -- print values of all hyperparameters (default: false)
+ - ``theoretical_estimates`` -- compute theoretical workfactors for all algorithms (default: false)
+ - ``use_mo`` -- use may-ozerov nearest neighbor search in theoretical workfactor computation (default: true)
+ - ``workfactor_accuracy`` -- the higher the more accurate the workfactor computation, can slow down computations significantly, recommended range 0-2 (needs to be larger than 0) (default: 1)
+ - ``limit_depth`` -- restricts BJMM and May-Ozerov algorithms to depth two only (default: false)
+ - ``quantum_estimates`` -- compute quantum estimates of all algorithms (default: true)
+ - ``maxdepth`` -- maximum allowed depth of the quantum circuit (default: 96)
+ - ``matrix_mult_constant`` -- used matrix multiplication constant (default: 2.5)
+ - ``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::
+
+ >>> from .estimator import *
+ >>> sd_estimate_display(n=600,k=400,w=22)
+ =========================================================================
+ Complexity estimation to solve the (600,400,22) syndrome decoding problem
+ =========================================================================
+ The following table states bit complexity estimates of the corresponding algorithms including an approximation of the polynomial factors inherent to the algorithm.
+ The quantum estimate gives a very optimistic estimation of the cost for a quantum aided attack with a circuit of limitted depth (should be understood as a lowerbound).
+ +----------------+---------------+---------+
+ | | estimate | quantum |
+ +----------------+------+--------+---------+
+ | algorithm | time | memory | time |
+ +----------------+------+--------+---------+
+ | Prange | 60.1 | 17.3 | 37.1 |
+ | Stern | 47.0 | 24.5 | -- |
+ | Dumer | 47.6 | 24.6 | -- |
+ | Ball Collision | 47.7 | 24.5 | -- |
+ | BJMM (MMT) | 47.6 | 22.7 | -- |
+ | BJMM-pdw | 47.7 | 21.7 | -- |
+ | May-Ozerov | 46.5 | 22.6 | -- |
+ | Both-May | 47.1 | 22.6 | -- |
+ +----------------+------+--------+---------+
+
+
+ >>> from .estimator import *
+ >>> sd_estimate_display(n=1000,k=500,w=100,all_parameters=1,theoretical_estimates=1,precision=2) # long time
+ ===========================================================================
+ Complexity estimation to solve the (1000,500,100) syndrome decoding problem
+ ===========================================================================
+ The following table states bit complexity estimates of the corresponding algorithms including an approximation of the polynomial factors inherent to the algorithm.
+ The approximation is based on the theoretical workfactor of the respective algorithms, disregarding all polynomial factors and using further approximations that introduce additional polynomial inaccurcies.
+ The quantum estimate gives a very optimistic estimation of the cost for a quantum aided attack with a circuit of limitted depth (should be understood as a lowerbound).
+ +----------------+-----------------+-----------------+---------+--------------------------------------------------------------------+
+ | | estimate | approximation | quantum | parameters |
+ +----------------+--------+--------+--------+--------+---------+--------------------------------------------------------------------+
+ | algorithm | time | memory | time | memory | time | classical |
+ +----------------+--------+--------+--------+--------+---------+--------------------------------------------------------------------+
+ | Prange | 134.46 | 19.26 | 108.03 | 0.00 | 76.39 | r : 7 |
+ | Stern | 117.04 | 38.21 | 104.02 | 31.39 | -- | l : 27 | p : 4 |
+ | Dumer | 116.82 | 38.53 | 103.76 | 33.68 | -- | l : 28 | p : 4 |
+ | Ball Collision | 117.04 | 38.21 | 103.76 | 32.67 | -- | l : 27 | p : 4 | pl : 0 |
+ | BJMM (MMT) | 112.39 | 73.15 | 90.17 | 67.76 | -- | l : 120 | p : 16 | p1 : 10 | depth : 2 |
+ | BJMM-pdw | 113.92 | 52.74 | -- | -- | -- | l1 : 35 | p : 10 | p1 : 6 | depth : 2 | l2 : 21 | w2 : 0 |
+ | May-Ozerov | 111.56 | 70.44 | 89.51 | 51.39 | -- | l : 69 | p : 14 | p1 : 10 | depth : 2 |
+ | Both-May | 113.68 | 68.58 | 87.60 | 64.13 | -- | l : 75 | p : 14 | p1 : 10 | w1 : 2 | w2 : 2 | depth : 2 |
+ +----------------+--------+--------+--------+--------+---------+--------------------------------------------------------------------+
+
+
+ TESTS::
+
+ >>> from .estimator import *
+ >>> sd_estimate_display(24646,12323,142,all_parameters=True) # long time
+ ==============================================================================
+ Complexity estimation to solve the (24646,12323,142) syndrome decoding problem
+ ==============================================================================
+ The following table states bit complexity estimates of the corresponding algorithms including an approximation of the polynomial factors inherent to the algorithm.
+ The quantum estimate gives a very optimistic estimation of the cost for a quantum aided attack with a circuit of limitted depth (should be understood as a lowerbound).
+ +----------------+----------------+---------+--------------------------------------------------------------------+
+ | | estimate | quantum | parameters |
+ +----------------+-------+--------+---------+--------------------------------------------------------------------+
+ | algorithm | time | memory | time | classical |
+ +----------------+-------+--------+---------+--------------------------------------------------------------------+
+ | Prange | 182.1 | 28.4 | 114.5 | r : 11 |
+ | Stern | 160.6 | 39.8 | -- | l : 33 | p : 2 |
+ | Dumer | 161.1 | 39.8 | -- | l : 28 | p : 2 |
+ | Ball Collision | 161.1 | 39.8 | -- | l : 28 | p : 2 | pl : 0 |
+ | BJMM (MMT) | 160.9 | 54.2 | -- | l : 74 | p : 4 | p1 : 3 | depth : 2 |
+ | BJMM-pdw | 160.9 | 55.0 | -- | l1 : 30 | p : 4 | p1 : 3 | depth : 2 | l2 : 22 | w2 : 0 |
+ | May-Ozerov | 160.4 | 55.0 | -- | l : 30 | p : 4 | p1 : 3 | depth : 2 |
+ | Both-May | 161.1 | 37.8 | -- | l : 4 | p : 2 | p1 : 1 | w1 : 1 | w2 : 0 | depth : 2 |
+ +----------------+-------+--------+---------+--------------------------------------------------------------------+
+
+
+ >>> from .estimator import *
+ >>> sd_estimate_display(300,200,20,all_parameters=True, skip=[])
+ =========================================================================
+ Complexity estimation to solve the (300,200,20) syndrome decoding problem
+ =========================================================================
+ The following table states bit complexity estimates of the corresponding algorithms including an approximation of the polynomial factors inherent to the algorithm.
+ The quantum estimate gives a very optimistic estimation of the cost for a quantum aided attack with a circuit of limitted depth (should be understood as a lowerbound).
+ +----------------+---------------+---------+-------------------------------------------------------------------------------------------+
+ | | estimate | quantum | parameters |
+ +----------------+------+--------+---------+-------------------------------------------------------------------------------------------+
+ | algorithm | time | memory | time | classical |
+ +----------------+------+--------+---------+-------------------------------------------------------------------------------------------+
+ | Prange | 52.5 | 15.3 | 33.5 | r : 5 |
+ | Stern | 40.7 | 21.5 | -- | l : 13 | p : 2 |
+ | Dumer | 41.1 | 26.9 | -- | l : 18 | p : 3 |
+ | Ball Collision | 41.3 | 21.5 | -- | l : 12 | p : 2 | pl : 0 |
+ | BJMM (MMT) | 41.1 | 27.5 | -- | l : 25 | p : 4 | p1 : 2 | depth : 2 |
+ | BJMM-pdw | 41.3 | 18.9 | -- | l1 : 3 | p : 2 | p1 : 1 | depth : 2 | l2 : 4 | w2 : 0 |
+ | BJMM-dw | 41.3 | 19.7 | -- | l : 6 | p : 2 | p1 : 1 | w1 : 0 | w11 : 1 | l2 : 5 | w2 : 0 | depth : 2 |
+ | May-Ozerov | 40.1 | 19.7 | -- | l : 2 | p : 2 | p1 : 1 | depth : 2 |
+ | Both-May | 40.4 | 19.7 | -- | l : 2 | p : 2 | p1 : 1 | w1 : 2 | w2 : 0 | depth : 2 |
+ +----------------+------+--------+---------+-------------------------------------------------------------------------------------------+
+
+
+
+ """
+
+ complexities = _sd_estimate(n, k, w, theoretical_estimates, memory_limit, bit_complexities, hmap, skip, use_mo,
+ workfactor_accuracy, limit_depth, quantum_estimates, maxdepth, matrix_mult_constant,
+ memory_access)
+
+ headline = "Complexity estimation to solve the ({},{},{}) syndrome decoding problem".format(n, k, w)
+ print("=" * len(headline))
+ print(headline)
+ print("=" * len(headline))
+ if bit_complexities:
+ print(
+ "The following table states bit complexity estimates of the corresponding algorithms including an approximation of the polynomial factors inherent to the algorithm.")
+ else:
+ print(
+ "The following table states complexity estimates of the corresponding algorithms including an approximation of the polynomial factors inherent to the algorithm.")
+ print("The time complexity estimate is measured in the number of additions in (F_2)^n.")
+ print("The memory complexity estimate is given in the number of vector space elements that need to be stored.")
+
+ if theoretical_estimates:
+ print(
+ "The approximation is based on the theoretical workfactor of the respective algorithms, disregarding all polynomial factors and using further approximations that introduce additional polynomial inaccurcies.")
+ if quantum_estimates:
+ print(
+ "The quantum estimate gives a very optimistic estimation of the cost for a quantum aided attack with a circuit of limitted depth (should be understood as a lowerbound).")
+ tables = []
+ table_fields = ['algorithm']
+
+ tbl_names = PrettyTable(table_fields)
+ tbl_names.padding_width = 1
+ tbl_names.title = ' '
+
+ for i in complexities.keys():
+ tbl_names.add_row([i])
+ tbl_names.align["algorithm"] = "l"
+ tables.append(tbl_names)
+
+ table_fields = ['time', 'memory']
+ tbl_estimates = PrettyTable(table_fields)
+ tbl_estimates.padding_width = 1
+ tbl_estimates.title = 'estimate'
+ tbl_estimates.align["time"] = "r"
+ tbl_estimates.align["memory"] = "r"
+ for i in complexities.keys():
+ if complexities[i]["time"] != inf:
+ T, M = __round_or_truncate_to_given_precision(complexities[i]["time"], complexities[i]["memory"], truncate,
+ precision)
+ else:
+ T, M = "--", "--"
+ tbl_estimates.add_row([T, M])
+
+ tables.append(tbl_estimates)
+
+ if theoretical_estimates:
+ table_fields = ['time', 'memory']
+ tbl_approx = PrettyTable(table_fields)
+ tbl_approx.padding_width = 1
+ tbl_approx.title = 'approximation'
+ tbl_approx.align["time"] = "r"
+ tbl_approx.align["memory"] = "r"
+
+ for i in complexities.keys():
+ if complexities[i]["Workfactor time"] != 0:
+ T, M = __round_or_truncate_to_given_precision(complexities[i]["Workfactor time"] * n,
+ complexities[i]["Workfactor memory"] * n, truncate,
+ precision)
+ else:
+ T, M = "--", "--"
+ tbl_approx.add_row([T, M])
+
+ tables.append(tbl_approx)
+
+ if quantum_estimates:
+ table_fields = [' time']
+ tbl_quantum = PrettyTable(table_fields)
+ tbl_quantum.padding_width = 1
+ tbl_quantum.title = "quantum"
+ tbl_quantum.align["time"] = "r"
+ for i in complexities.keys():
+ if "quantum time" in complexities[i].keys() and complexities[i]["quantum time"] != 0:
+ T, M = __round_or_truncate_to_given_precision(complexities[i]["quantum time"], 0, truncate, precision)
+ else:
+ T = "--"
+ tbl_quantum.add_row([T])
+ tables.append(tbl_quantum)
+
+ if all_parameters:
+ table_fields = ['classical']
+ tbl_params = PrettyTable(table_fields)
+ tbl_params.padding_width = 1
+ tbl_params.title = "parameters"
+ tbl_params.align['classical'] = "l"
+
+ for i in complexities.keys():
+ row = ""
+ for j in complexities[i]["parameters"].keys():
+ row += "{:<{align}}".format(j, align=max(2, len(j))) + " : " + '{:3d}'.format(
+ complexities[i]["parameters"][j]) + " | "
+ tbl_params.add_row([row[:-3]])
+
+ tables.append(tbl_params)
+
+ tbl_join = __concat_pretty_tables(str(tables[0]), str(tables[1]))
+ for i in range(2, len(tables)):
+ tbl_join = __concat_pretty_tables(tbl_join, str(tables[i]))
+
+ print(tbl_join)
+
+
+def _add_theoretical_estimates(complexities, n, k, w, memory_limit, skip, use_mo, workfactor_accuracy):
+ rate = k / n
+ omega = w / n
+
+ grid_std_accuracy = {"prange": [20, 150], "stern": [20, 150], "dumer": [20, 150], "ball_collision": [15, 150],
+ "bjmm": [10, 250], "may-ozerov": [5, 1000], "both-may": [5, 1000]}
+
+ if workfactor_accuracy != 1:
+ for i in grid_std_accuracy.keys():
+ for j in range(2):
+ grid_std_accuracy[i][j] = int(ceil(grid_std_accuracy[i][j] * workfactor_accuracy))
+
+ for i in complexities.keys():
+ complexities[i]["Workfactor time"] = 0
+ complexities[i]["Workfactor memory"] = 0
+
+ nr_algorithms = 7 - len(skip)
+ nr_algorithms += 1 if "BJMM-dw" in skip else 0
+ nr_algorithms += 1 if "BJMM-p-dw" in skip or "BJMM-pdw" in skip else 0
+ bar = Bar('Computing theoretical workfactors\t', max=nr_algorithms)
+
+ if "prange" not in skip:
+ T, M = prange_workfactor(rate, omega, grid_std_accuracy["prange"][0], grid_std_accuracy["prange"][1],
+ memory_limit)
+ complexities["Prange"]["Workfactor time"] = T
+ complexities["Prange"]["Workfactor memory"] = M
+ bar.next()
+ if "stern" not in skip:
+ T, M = stern_workfactor(rate, omega, grid_std_accuracy["stern"][0], grid_std_accuracy["stern"][1], memory_limit)
+ complexities["Stern"]["Workfactor time"] = T
+ complexities["Stern"]["Workfactor memory"] = M
+ bar.next()
+ if "dumer" not in skip:
+ T, M = dumer_workfactor(rate, omega, grid_std_accuracy["dumer"][0], grid_std_accuracy["dumer"][1], memory_limit)
+ complexities["Dumer"]["Workfactor time"] = T
+ complexities["Dumer"]["Workfactor memory"] = M
+ bar.next()
+ if "ball_collision" not in skip:
+ T, M = ball_collision_workfactor(rate, omega, grid_std_accuracy["ball_collision"][0],
+ grid_std_accuracy["ball_collision"][1], memory_limit)
+ complexities["Ball Collision"]["Workfactor time"] = T
+ complexities["Ball Collision"]["Workfactor memory"] = M
+ bar.next()
+ if "BJMM" not in skip and "MMT" not in skip:
+ T, M = bjmm_workfactor(rate, omega, grid_std_accuracy["bjmm"][0], grid_std_accuracy["bjmm"][1], memory_limit)
+ complexities["BJMM (MMT)"]["Workfactor time"] = T
+ complexities["BJMM (MMT)"]["Workfactor memory"] = M
+ bar.next()
+ if "MO" not in skip and "May-Ozerov" not in skip:
+ T, M = may_ozerov_workfactor(rate, omega, grid_std_accuracy["may-ozerov"][0],
+ grid_std_accuracy["may-ozerov"][1], memory_limit, use_mo)
+ complexities["May-Ozerov"]["Workfactor time"] = T
+ complexities["May-Ozerov"]["Workfactor memory"] = M
+ bar.next()
+ if "BM" not in skip and "Both-May" not in skip:
+ T, M = both_may_workfactor(rate, omega, grid_std_accuracy["both-may"][0], grid_std_accuracy["both-may"][1],
+ memory_limit, use_mo)
+ complexities["Both-May"]["Workfactor time"] = T
+ complexities["Both-May"]["Workfactor memory"] = M
+ bar.next()
+
+ bar.finish()
+
+
+def _sd_estimate(n, k, w, theoretical_estimates, memory_limit, bit_complexities, hmap, skip, use_mo,
+ workfactor_accuracy, limit_depth, quantum_estimates, maxdepth, matrix_mult_constant, memory_access):
+ """
+ Estimate complexity to solve syndrome decoding problem
+
+ INPUT:
+
+ - ``n`` -- length of the code
+ - ``k`` -- dimension of the code
+ - ``w`` -- Hamming weight of error vector
+ - ``memory_limit`` -- upper bound on the available memory (as log2(bits))
+ - ``hmap`` -- indicates if hashmap should be used for sorting lists
+ - ``skip`` -- list of algorithms not to consider
+ - ``use_mo`` -- use may-ozerov nearest neighbor search in theoretical workfactor computation
+ - ``workfactor_accuracy`` -- the higher the more accurate the workfactor computation, can slow down computations significantly, recommended range 0-2 (needs to be larger than 0)
+
+ """
+
+ complexities = {}
+ if bit_complexities:
+ memory_limit -= log2(n)
+
+ nr_algorithms = 9 - len(skip)
+ bar = Bar('Computing estimates\t\t\t', max=nr_algorithms)
+
+ if "prange" not in skip:
+ complexities["Prange"] = prange_complexity(n, k, w, mem=memory_limit, memory_access=memory_access)
+ if quantum_estimates:
+ complexities["Prange"]["quantum time"] = quantum_prange_complexity(n, k, w, maxdepth=maxdepth,
+ matrix_mult_constant=matrix_mult_constant)
+ bar.next()
+
+ if "stern" not in skip:
+ complexities["Stern"] = stern_complexity(n, k, w, mem=memory_limit, hmap=hmap, memory_access=memory_access)
+ bar.next()
+ if "dumer" not in skip:
+ complexities["Dumer"] = dumer_complexity(n, k, w, mem=memory_limit, hmap=hmap, memory_access=memory_access)
+ bar.next()
+ if "ball_collision" not in skip:
+ complexities["Ball Collision"] = ball_collision_decoding_complexity(n, k, w, mem=memory_limit, hmap=hmap,
+ memory_access=memory_access)
+ bar.next()
+ if "BJMM" not in skip and "MMT" not in skip:
+ complexities["BJMM (MMT)"] = bjmm_complexity(n, k, w, mem=memory_limit, hmap=hmap, only_depth_two=limit_depth,
+ memory_access=memory_access)
+ bar.next()
+ if "BJMM-pdw" not in skip and "BJMM-p-dw" not in skip:
+ complexities["BJMM-pdw"] = bjmm_depth_2_partially_disjoint_weight_complexity(n, k, w, mem=memory_limit,
+ hmap=hmap,
+ memory_access=memory_access)
+ bar.next()
+ if "BJMM-dw" not in skip:
+ complexities["BJMM-dw"] = bjmm_depth_2_disjoint_weight_complexity(n, k, w, mem=memory_limit, hmap=hmap,
+ memory_access=memory_access)
+ bar.next()
+ if "MO" not in skip and "May-Ozerov" not in skip:
+ complexities["May-Ozerov"] = may_ozerov_complexity(n, k, w, mem=memory_limit, hmap=hmap,
+ only_depth_two=limit_depth, memory_access=memory_access)
+ bar.next()
+ if "BM" not in skip and "Both-May" not in skip:
+ complexities["Both-May"] = both_may_depth_2_complexity(n, k, w, mem=memory_limit, hmap=hmap,
+ memory_access=memory_access)
+ bar.next()
+
+ bar.finish()
+ if theoretical_estimates:
+ _add_theoretical_estimates(complexities, n, k, w, memory_limit, skip, use_mo, workfactor_accuracy)
+
+ if bit_complexities:
+ field_op = log2(n)
+ for i in complexities.keys():
+ complexities[i]["time"] += field_op
+ complexities[i]["memory"] += field_op
+
+ return complexities
diff --git a/tests/module/kmp_cost.sage b/tests/module/kmp_cost.sage
new file mode 100644
index 00000000..231f5e8d
--- /dev/null
+++ b/tests/module/kmp_cost.sage
@@ -0,0 +1,95 @@
+load('tests/module/cost.sage');
+
+def binary_entropy(x):
+
+ return -x*log2(x)-(1-x)*log2(1-x);
+
+###################################################
+
+def gauss_binomial(m,r,q):
+
+ x = 1.;
+ for i in range(r):
+ x = x*(1.-q^(m-i*1.))/(1.-q^(1.+i));
+
+ return x;
+
+##################################
+
+def binary_entropy(x):
+
+ return -x*log(x*1.)/log(2.) -(1-x)*log(1-x*1.)/log(2.);
+
+########################################
+
+
+#Cost of KMP algorithm: the estimate is obtained considering the numerical optimization of the algorithm running time
+
+def kmp_cost_numerical(n,r,ell,q):
+
+
+ best_cost = 10000000000000000; #lowest running time of the algorithm
+
+ #Parameters that optimize the attack
+ best_u = -1;
+ best_u1 = -1;
+
+ #List size and number of collisions
+ best_L1 = -1;
+ best_L2 = -1;
+ best_num_coll = -1;
+
+ for u in range(2,r+1):
+
+
+ u1 = floor((n-r+u)/2);
+ u2 = n-r+u-u1;
+
+ L1 = log2(factorial(n)/factorial(n-u1));
+ L2 = log2(factorial(n)/factorial(n-u2));
+ num_coll = log2(factorial(n)*factorial(n)/factorial(n-u1)/factorial(n-u2)*q^(ell*(n-r-u1-u2)) );
+
+ cost = log2(2^L1+2^L2+2^num_coll);
+
+ if cost < best_cost:
+ best_u1 = u1;
+ best_u2 = u2;
+ best_cost = cost;
+ best_L1 = L1;
+ best_L2 = L2;
+ best_num_coll = num_coll;
+
+ return best_u1, best_u2, best_L1, best_L1, best_num_coll, best_cost;
+
+
+#######################################
+
+#Cost of KMP algorithm: the estimate is obtained considering the asymptotic running time
+
+def kmp_cost_asymptotic(n,r,ell,q):
+
+ R = 1-r/n;
+
+ mu_val = var('mu_val');
+
+ #the optimal value for mu is found by numerical root finding
+ th_mu_val = find_root(binary_entropy(mu_val)+mu_val*log(mu_val*n/exp(1.))/log(2.)+ell*log(q*1.)/log(2.)*(R-2*mu_val),0, 1);
+
+ th_coeff = binary_entropy(th_mu_val)+th_mu_val*log2(th_mu_val*n/exp(1.));
+
+ return th_coeff*n;
+######################################################
+
+#Uncomment the following lines for a quick test
+
+#n = 94;
+#r = 55;
+#q = 509;
+#ell = 1;
+
+
+#best_u1, best_u2, best_L1, best_L1, best_num_coll, best_cost = kmp_cost_numerical(n,r,ell,q);
+#th_cost = kmp_cost_asymptotic(n,r,ell,q);
+
+#print("Numerical best cost for KMP = 2^",best_cost);
+#print("Asymptotic estimate = 2^",th_cost);
diff --git a/tests/module/multivariate_quadratic_estimator/COPYING b/tests/module/multivariate_quadratic_estimator/COPYING
new file mode 100644
index 00000000..f288702d
--- /dev/null
+++ b/tests/module/multivariate_quadratic_estimator/COPYING
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ 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 .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/tests/module/multivariate_quadratic_estimator/mpkc/__init__.py b/tests/module/multivariate_quadratic_estimator/mpkc/__init__.py
new file mode 100644
index 00000000..0e119576
--- /dev/null
+++ b/tests/module/multivariate_quadratic_estimator/mpkc/__init__.py
@@ -0,0 +1,22 @@
+# *****************************************************************************
+# Multivariate Quadratic (MQ) Estimator
+# Copyright (C) 2021-2022 Emanuele Bellini, Rusydi H. Makarim, Javier Verbel
+# Cryptography Research Centre, Technology Innovation Institute LLC
+#
+# This file is part of MQ Estimator
+#
+# MQ Estimator 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.
+#
+# MQ Estimator 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
+# MQ Estimator. If not, see .
+# *****************************************************************************
+
+
+from .mq_estimator import MQEstimator
diff --git a/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/__init__.py b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/__init__.py
new file mode 100644
index 00000000..13004f08
--- /dev/null
+++ b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/__init__.py
@@ -0,0 +1,33 @@
+# *****************************************************************************
+# Multivariate Quadratic (MQ) Estimator
+# Copyright (C) 2021-2022 Emanuele Bellini, Rusydi H. Makarim, Javier Verbel
+# Cryptography Research Centre, Technology Innovation Institute LLC
+#
+# This file is part of MQ Estimator
+#
+# MQ Estimator 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.
+#
+# MQ Estimator 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
+# MQ Estimator. If not, see .
+# *****************************************************************************
+
+
+from .f5 import F5
+from .hybrid_f5 import HybridF5
+from .kpg import KPG
+from .mht import MHT
+from .dinur1 import DinurFirst
+from .dinur2 import DinurSecond
+from .exhaustive_search import ExhaustiveSearch
+from .cgmta import CGMTA
+from .bjorklund import Bjorklund
+from .lokshtanov import Lokshtanov
+from .boolean_solve_fxl import BooleanSolveFXL
+from .crossbred import Crossbred
diff --git a/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/base.py b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/base.py
new file mode 100644
index 00000000..1c72688d
--- /dev/null
+++ b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/base.py
@@ -0,0 +1,341 @@
+# *****************************************************************************
+# Multivariate Quadratic (MQ) Estimator
+# Copyright (C) 2021-2022 Emanuele Bellini, Rusydi H. Makarim, Javier Verbel
+# Cryptography Research Centre, Technology Innovation Institute LLC
+#
+# This file is part of MQ Estimator
+#
+# MQ Estimator 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.
+#
+# MQ Estimator 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
+# MQ Estimator. If not, see .
+# *****************************************************************************
+
+
+import functools
+
+from sage.arith.misc import is_prime_power
+from sage.functions.other import floor
+
+
+class BaseAlgorithm:
+ def __init__(self, n, m, q=None, w=None, h=0):
+ """
+ Base class for algorithms complexity estimator
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``m`` -- no. of polynomials
+ - ``q`` -- order of the field (default: None)
+ - ``w`` -- linear algebra constant (default: None)
+ - ``h`` -- external hybridization parameter (default: 0)
+
+ TESTS::
+
+ sage: from mpkc.algorithms.base import BaseAlgorithm
+ sage: BaseAlgorithm(n=-1, m=5)
+ Traceback (most recent call last):
+ ...
+ ValueError: n must be >= 1
+ sage: BaseAlgorithm(n=5, m=0)
+ Traceback (most recent call last):
+ ...
+ ValueError: m must be >= 1
+ sage: BaseAlgorithm(n=5, m=10, q=6)
+ Traceback (most recent call last):
+ ...
+ ValueError: q must be a prime power
+ sage: BaseAlgorithm(n=5, m=10, w=1)
+ Traceback (most recent call last):
+ ...
+ ValueError: w must be in the range 2 <= w <= 3
+ """
+
+ if n < 1:
+ raise ValueError("n must be >= 1")
+
+ if m < 1:
+ raise ValueError("m must be >= 1")
+
+ if q is not None and not is_prime_power(q):
+ raise ValueError("q must be a prime power")
+
+ if w is not None and not 2 <= w <= 3:
+ raise ValueError("w must be in the range 2 <= w <= 3")
+
+ if h < 0:
+ raise ValueError("h must be >= 0")
+
+ self._n = n
+ self._m = m
+ self._q = q
+ self._w = w
+ self._h = h
+ self._optimal_parameters = dict()
+ self._n_reduced = None
+ self._m_reduced = None
+
+ def nvariables(self):
+ """
+ Return the number of variables
+
+ TESTS::
+
+ sage: from mpkc.algorithms.base import BaseAlgorithm
+ sage: BaseAlgorithm(n=10, m=5).nvariables()
+ 10
+ """
+ return self._n
+
+ def nvariables_reduced(self):
+ """
+ Return the no. of variables after fixing some values
+
+ TESTS::
+
+ sage: from mpkc.algorithms.base import BaseAlgorithm
+ sage: BaseAlgorithm(n=5, m=10).nvariables_reduced()
+ 5
+ sage: BaseAlgorithm(n=25, m=20).nvariables_reduced()
+ 20
+ """
+ if self._n_reduced is not None:
+ return self._n_reduced
+
+ n, m = self.nvariables(), self.npolynomials()
+ if self.is_underdefined_system():
+ alpha = floor(n / m)
+ self._n_reduced = m - alpha + 1
+ else:
+ self._n_reduced = n
+
+ self._n_reduced -= self._h
+ return self._n_reduced
+
+ def npolynomials_reduced(self):
+ """
+ Return the no. of polynomials after applying the Thomae and Wolf strategy
+
+ TESTS::
+
+ sage: from mpkc.algorithms.base import BaseAlgorithm
+ sage: BaseAlgorithm(n=5, m=10).npolynomials_reduced()
+ 10
+ sage: BaseAlgorithm(n=60, m=20).npolynomials_reduced()
+ 18
+ """
+ if self._m_reduced is not None:
+ return self._m_reduced
+
+ n, m = self.nvariables(), self.npolynomials()
+ if self.is_underdefined_system():
+ self._m_reduced = self.nvariables_reduced()
+ else:
+ self._m_reduced = m
+ return self._m_reduced
+
+ def npolynomials(self):
+ """"
+ Return the number of polynomials
+
+ TESTS::
+
+ sage: from mpkc.algorithms.base import BaseAlgorithm
+ sage: BaseAlgorithm(n=10, m=5).npolynomials()
+ 5
+ """
+ return self._m
+
+ def time_complexity(self, **kwargs):
+ """
+ Return the time complexity of the algorithm
+
+ TESTS::
+
+ sage: from mpkc.algorithms.base import BaseAlgorithm
+ sage: BaseAlgorithm(n=10, m=5).time_complexity()
+ Traceback (most recent call last):
+ ...
+ NotImplementedError
+ """
+ raise NotImplementedError
+
+ def memory_complexity(self):
+ """
+ Return the memory complexity of the algorithm
+
+ TESTS::
+
+ sage: from mpkc.algorithms.base import BaseAlgorithm
+ sage: BaseAlgorithm(n=10, m=5).memory_complexity()
+ Traceback (most recent call last):
+ ...
+ NotImplementedError
+ """
+ raise NotImplementedError
+
+ def tilde_o_time(self):
+ """
+ Return the Ε time complexity
+
+ TESTS::
+
+ sage: from mpkc.algorithms.base import BaseAlgorithm
+ sage: BaseAlgorithm(n=10, m=5).tilde_o_time()
+ Traceback (most recent call last):
+ ...
+ NotImplementedError
+ """
+ raise NotImplementedError
+
+ def order_of_the_field(self):
+ """
+ Return the order of the field
+
+ TESTS::
+
+ sage: from mpkc.algorithms.base import BaseAlgorithm
+ sage: BaseAlgorithm(n=10, m=5).order_of_the_field()
+
+ sage: BaseAlgorithm(n=10, m=5, q=256).order_of_the_field()
+ 256
+ """
+ return self._q
+
+ def is_defined_over_finite_field(self):
+ """
+ Return `True` if the algorithm is defined over a finite field
+
+ TESTS::
+
+ sage: from mpkc.algorithms.base import BaseAlgorithm
+ sage: BaseAlgorithm(n=10, m=5).is_defined_over_finite_field()
+ False
+ sage: BaseAlgorithm(n=10, m=5, q=256).is_defined_over_finite_field()
+ True
+ """
+ return self.order_of_the_field() is not None
+
+ def linear_algebra_constant(self):
+ """
+ Return the linear algebra constant
+
+ TESTS::
+
+ sage: from mpkc.algorithms.base import BaseAlgorithm
+ sage: BaseAlgorithm(n=10, m=5).linear_algebra_constant()
+
+ sage: BaseAlgorithm(n=10, m=5, w=2).linear_algebra_constant()
+ 2
+ """
+ return self._w
+
+ def is_overdefined_system(self):
+ """
+ Return `True` if the system is overdefined
+
+ TESTS::
+
+ sage: from mpkc.algorithms.base import BaseAlgorithm
+ sage: BaseAlgorithm(n=5, m=10).is_overdefined_system()
+ True
+ sage: BaseAlgorithm(n=10, m=5).is_overdefined_system()
+ False
+ sage: BaseAlgorithm(n=10, m=10).is_overdefined_system()
+ False
+ """
+ return self.npolynomials() > self.nvariables()
+
+ def is_underdefined_system(self):
+ """
+ Return `True` if the system is underdefined
+
+ TESTS::
+
+ sage: from mpkc.algorithms.base import BaseAlgorithm
+ sage: BaseAlgorithm(n=10, m=5).is_underdefined_system()
+ True
+ sage: BaseAlgorithm(n=5, m=10).is_underdefined_system()
+ False
+ sage: BaseAlgorithm(n=10, m=10).is_underdefined_system()
+ False
+ """
+ return self.nvariables() > self.npolynomials()
+
+ def is_square_system(self):
+ """
+ Return `True` is the system is square, i.e. there are equal no. of variables and polynomials
+
+ TESTS::
+
+ sage: from mpkc.algorithms.base import BaseAlgorithm
+ sage: BaseAlgorithm(n=10, m=10).is_square_system()
+ True
+ sage: BaseAlgorithm(n=5, m=10).is_square_system()
+ False
+ """
+ return self.nvariables() == self.npolynomials()
+
+ def optimal_parameters(self):
+ """
+ Return a dictionary of optimal parameters
+
+ TESTS::
+
+ sage: from mpkc.algorithms.base import BaseAlgorithm
+ sage: BaseAlgorithm(n=10, m=10).optimal_parameters()
+ {}
+ """
+ if self.has_optimal_parameter() and not self._optimal_parameters:
+ for f in self._optimal_parameter_methods_():
+ _ = f()
+ return self._optimal_parameters
+
+ def has_optimal_parameter(self):
+ """
+ Return `True` if the algorithm has optimal parameter
+
+ TESTS::
+
+ sage: from mpkc.algorithms.base import BaseAlgorithm
+ sage: BaseAlgorithm(n=10, m=10).has_optimal_parameter()
+ False
+ """
+ return len(self._optimal_parameter_methods_()) > 0
+
+ def _optimal_parameter_methods_(self):
+ """
+ Return a list of methods decorated with @optimal_parameter
+ """
+ import inspect
+
+ def is_optimal_parameter_method(object):
+ return inspect.ismethod(object) and hasattr(object, "__wrapped__")
+ return [f for (_, f) in inspect.getmembers(self, predicate=is_optimal_parameter_method)]
+
+
+def optimal_parameter(func):
+ """
+ Decorator to indicate optimization parameter in BaseAlgorithm
+
+ INPUT:
+
+ - ``f`` -- a method of a BaseAlgoritm subclass
+ """
+ @functools.wraps(func)
+ def optimal_parameter(*args, **kwargs):
+ name = func.__name__
+ self = args[0]
+
+ if name not in self._optimal_parameters:
+ self._optimal_parameters[name] = func(*args, **kwargs)
+ return self._optimal_parameters[name]
+ return optimal_parameter
diff --git a/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/bjorklund.py b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/bjorklund.py
new file mode 100644
index 00000000..3d0e8887
--- /dev/null
+++ b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/bjorklund.py
@@ -0,0 +1,385 @@
+# *****************************************************************************
+# Multivariate Quadratic (MQ) Estimator
+# Copyright (C) 2021-2022 Emanuele Bellini, Rusydi H. Makarim, Javier Verbel
+# Cryptography Research Centre, Technology Innovation Institute LLC
+#
+# This file is part of MQ Estimator
+#
+# MQ Estimator 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.
+#
+# MQ Estimator 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
+# MQ Estimator. If not, see .
+# *****************************************************************************
+
+
+from sage.rings.infinity import Infinity
+from sage.functions.log import log
+from sage.functions.other import floor
+from .base import BaseAlgorithm, optimal_parameter
+from ..utils import sum_of_binomial_coefficients
+
+
+class Bjorklund(BaseAlgorithm):
+ r"""
+ Construct an instance of Bjorklund et al.'s estimator
+
+ Bjorklund et al.'s is a probabilistic algorithm to solve the MQ problem of GF(2) [BKW19]_. It finds a solution of a qudractic
+ system by computing the parity of it number of solutions.
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``m`` -- no. of polynomials
+ - ``nsolutions`` -- number of solutions (default: 1)
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Bjorklund
+ sage: E = Bjorklund(n=10, m=12)
+ sage: E
+ BjΓΆrklund et al.'s estimator for the MQ problem
+ """
+ def __init__(self, n, m, nsolutions=1, h=0):
+ super().__init__(n=n, m=m, q=2, h=h)
+ self._nsolutions = nsolutions
+ self._k = floor(log(nsolutions + 1, 2))
+ self._time_complexity = None
+ self._memory_complexity = None
+ self._Ξ» = None
+
+ def nsolutions(self):
+ """
+ Return the number of solutions
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Bjorklund
+ sage: B = Bjorklund(n=10, m=12, nsolutions=3)
+ sage: B.nsolutions()
+ 3
+ """
+ return self._nsolutions
+
+ @optimal_parameter
+ def Ξ»(self):
+ """
+ Return the optimal Ξ»
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Bjorklund
+ sage: E = Bjorklund(n=10, m=12)
+ sage: E.Ξ»()
+ 3/10
+ """
+ if self._Ξ» is not None:
+ return self._Ξ»
+
+ n, m = self.nvariables_reduced(), self.npolynomials_reduced()
+ k = self._k
+ min_complexity = Infinity
+ optimal_Ξ» = None
+
+ for l in range(3, min(m, n - 1)):
+ Ξ»_ = l / n
+ complexity = self._time_complexity_(Ξ»_)
+ if complexity < min_complexity:
+ min_complexity = complexity
+ optimal_Ξ» = Ξ»_
+
+ self._Ξ» = optimal_Ξ»
+ return self._Ξ»
+
+ def time_complexity(self, **kwargs):
+ """
+ Return the time complexity of Bjorklund et al.'s algorithm
+
+ INPUT:
+
+ - ``Ξ»`` -- the Ξ» value (default: None)
+
+ If Ξ» is specified, the function returns the time complexity w.r.t. the given parameter
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Bjorklund
+ sage: E = Bjorklund(n=10, m=12)
+ sage: float(log(E.time_complexity(), 2))
+ 35.48523010807851
+ sage: float(log(E.time_complexity(Ξ»=7/10), 2))
+ 49.97565549640329
+
+ TESTS::
+
+ sage: E0 = Bjorklund(n=15, m=12)
+ sage: E1 = Bjorklund(n=16, m=12)
+ sage: E0.time_complexity().numerical_approx() == E1.time_complexity().numerical_approx()
+ True
+ """
+ Ξ» = kwargs.get('Ξ»', None)
+
+ if Ξ» is not None:
+ time_complexity = self._time_complexity_(Ξ»)
+ else:
+ if self._time_complexity is not None:
+ time_complexity = self._time_complexity
+ else:
+ time_complexity = self._time_complexity = self._time_complexity_(self.Ξ»())
+
+ h = self._h
+ time_complexity *= 2 ** h
+ return time_complexity
+
+ def memory_complexity(self):
+ """
+ Return the memory complexity of Bjorklund et al.'s algorithm
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Bjorklund
+ sage: E = Bjorklund(n=10, m=12)
+ sage: float(log(E.memory_complexity(), 2))
+ 10.89550378006907
+
+ TESTS::
+
+ sage: E0 = Bjorklund(n=15, m=12)
+ sage: E1 = Bjorklund(n=16, m=12)
+ sage: E0.memory_complexity().numerical_approx() == E1.memory_complexity().numerical_approx()
+ True
+ """
+ if self._memory_complexity is not None:
+ return self._memory_complexity
+
+ def S(_n, _m, _Ξ»):
+ if _n <= 1:
+ return 0
+ else:
+ s = 48 * _n + 1
+ l = floor(_Ξ» * _n)
+ return S(l, l + 2, _Ξ») + 2 ** (_n - l) * log(s, 2) + _m * sum_of_binomial_coefficients(_n, 2)
+
+ n, m = self.nvariables_reduced(), self.npolynomials_reduced()
+ Ξ» = self.Ξ»()
+ self._memory_complexity = S(n, m, Ξ»)
+ return self._memory_complexity
+
+ def tilde_o_time(self):
+ """
+ Return the Ε time complexity of Bjorklund et al.'s algorithm
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Bjorklund
+ sage: E = Bjorklund(n=10, m=12)
+ sage: float(log(E.tilde_o_time(), 2))
+ 8.03225
+ """
+ n = self.nvariables_reduced()
+ h = self._h
+ return 2 ** h * 2 ** (0.803225 * n)
+
+ @staticmethod
+ def _T(n, m, Ξ»):
+ if n <= 1:
+ return 1
+ else:
+ l = floor(Ξ» * n)
+ T1 = (n + (l + 2) * m * sum_of_binomial_coefficients(n, 2) + (n - l) * 2 ** (n - l))
+ s = 48 * n + 1
+ return s * sum_of_binomial_coefficients(n - l, l + 4) * (Bjorklund._T(l, l + 2, Ξ») + T1)
+
+ def _time_complexity_(self, Ξ»):
+ """
+ Return the time complexity w.r.t. Ξ»
+
+ INPUT:
+
+ - ``Ξ»`` -- the Ξ» value
+ """
+ n, m = self.nvariables_reduced(), self.npolynomials_reduced()
+ k = self._k
+
+ return 8 * k * log(n, 2) * sum([Bjorklund._T(n - i, m + k + 2, Ξ») for i in range(1, n)])
+
+ def __repr__(self):
+ return f"BjΓΆrklund et al.'s estimator for the MQ problem"
+
+ # all methods below are implemented to overwrite the parent's docstring while keeping the implementation
+
+ def has_optimal_parameter(self):
+ """
+ Return `True` if the algorithm has optimal parameter
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Bjorklund
+ sage: H = Bjorklund(n=5, m=10)
+ sage: H.has_optimal_parameter()
+ True
+ """
+ return super().has_optimal_parameter()
+
+ def is_defined_over_finite_field(self):
+ """
+ Return `True` if the algorithm is defined over a finite field
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Bjorklund
+ sage: H = Bjorklund(n=5, m=10)
+ sage: H.is_defined_over_finite_field()
+ True
+ """
+ return super().is_defined_over_finite_field()
+
+ def is_overdefined_system(self):
+ """
+ Return `True` if the system is overdefined
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Bjorklund
+ sage: H = Bjorklund(n=5, m=10)
+ sage: H.is_overdefined_system()
+ True
+ sage: E = Bjorklund(n=10, m=10)
+ sage: E.is_overdefined_system()
+ False
+ """
+ return super().is_overdefined_system()
+
+ def is_square_system(self):
+ """
+ Return `True` if the system is square, there are equal no. of variables and polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Bjorklund
+ sage: H = Bjorklund(n=5, m=10)
+ sage: H.is_square_system()
+ False
+ sage: E = Bjorklund(n=10, m=10)
+ sage: E.is_square_system()
+ True
+ """
+ return super().is_square_system()
+
+ def is_underdefined_system(self):
+ """
+ Return `True` if the system is underdefined
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Bjorklund
+ sage: H = Bjorklund(n=5, m=10)
+ sage: H.is_underdefined_system()
+ False
+ sage: E = Bjorklund(n=10, m=5)
+ sage: E.is_underdefined_system()
+ True
+ """
+ return super().is_underdefined_system()
+
+ def linear_algebra_constant(self):
+ """
+ Return the linear algebra constant
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Bjorklund
+ sage: H = Bjorklund(n=5, m=10)
+ sage: H.linear_algebra_constant()
+
+ """
+
+ def npolynomials(self):
+ """
+ Return the number of polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Bjorklund
+ sage: H = Bjorklund(n=5, m=10)
+ sage: H.npolynomials()
+ 10
+ """
+ return super().npolynomials()
+
+ def nvariables(self):
+ """
+ Return the number of variables
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Bjorklund
+ sage: H = Bjorklund(n=5, m=10)
+ sage: H.nvariables()
+ 5
+ """
+ return super().nvariables()
+
+ def nvariables_reduced(self):
+ """
+ Return the no. of variables after fixing some values
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Bjorklund
+ sage: H = Bjorklund(n=5, m=10)
+ sage: H.nvariables_reduced()
+ 5
+ sage: E = Bjorklund(n=12, m=10)
+ sage: E.nvariables_reduced()
+ 10
+ """
+ return super().nvariables_reduced()
+
+ def npolynomials_reduced(self):
+ """
+ Return the no. of polynomials after applying the Thomae and Wolf strategy
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Bjorklund
+ sage: H = Bjorklund(n=5, m=10)
+ sage: H.npolynomials_reduced()
+ 10
+ sage: E = Bjorklund(n=12, m=10)
+ sage: E.npolynomials_reduced()
+ 10
+ """
+ return super().npolynomials_reduced()
+
+ def optimal_parameters(self):
+ """
+ Return a dictionary of optimal parameters
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Bjorklund
+ sage: H = Bjorklund(n=15, m=10)
+ sage: H.optimal_parameters()
+ {'Ξ»': 3/10}
+ """
+ return super().optimal_parameters()
+
+ def order_of_the_field(self):
+ """
+ Return the order of the field
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Bjorklund
+ sage: H = Bjorklund(n=15, m=10)
+ sage: H.order_of_the_field()
+ 2
+ """
+ return super().order_of_the_field()
diff --git a/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/boolean_solve_fxl.py b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/boolean_solve_fxl.py
new file mode 100644
index 00000000..5bd8a16f
--- /dev/null
+++ b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/boolean_solve_fxl.py
@@ -0,0 +1,423 @@
+# *****************************************************************************
+# Multivariate Quadratic (MQ) Estimator
+# Copyright (C) 2021-2022 Emanuele Bellini, Rusydi H. Makarim, Javier Verbel
+# Cryptography Research Centre, Technology Innovation Institute LLC
+#
+# This file is part of MQ Estimator
+#
+# MQ Estimator 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.
+#
+# MQ Estimator 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
+# MQ Estimator. If not, see .
+# *****************************************************************************
+
+
+"""
+Module to compute the time and memory complexity of the algorithms BooleanSolve and FXL
+
+The BooleanSolve and the FXL are algorithms to solve the MQ problem
+
+[BFS+11] Bardet, M., Faugère, J.-C., Salvy, B., and Spaenlehauer, P.-J. On the complexity of solving quadratic
+boolean systems. CoRR,abs/1112.6263, 2011.
+
+[YC04] Courtois, N., and Klimov, A., and Patarin, J., and Shamir, A. Efficient algorithms for solving overdefined
+systems of multivariate polynomial equations, In B. Preneel, editor,Advancesin Cryptology β EUROCRYPT 2000,
+pages 392β407, Berlin, Heidelberg, 2000. SpringerBerlin Heidelberg.
+
+For the space complexity of the variant las_vegas
+
+[Nie12] Niederhagen, R. Parallel Cryptanalysis. PhD thesis, Eindhoven University of Technology, 2012.
+http://polycephaly.org/thesis/index.shtml.30
+
+"""
+from sage.all import Integer
+from sage.arith.misc import binomial
+from sage.functions.log import log
+from sage.rings.infinity import Infinity
+from .base import BaseAlgorithm, optimal_parameter
+from .. import witness_degree
+
+
+class BooleanSolveFXL(BaseAlgorithm):
+ """
+ Construct an instance of BooleanSolve and FXL estimator
+
+ BooleanSolve and FXL are algorithms to solve the MQ problem over GF(2) and GF(q), respectively [BFSS11]_ [CKPS]_.
+ They work by guessing the value of $k$ variables and computing the consistency of the resulting subsystem.
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``m`` -- no. of polynomials
+ - ``q`` -- order of the finite field
+ - ``w`` -- linear algebra constant (2 <= w <= 3) (default: 2)
+ - ``h`` -- external hybridization parameter (default: 0)
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import BooleanSolveFXL
+ sage: E = BooleanSolveFXL(n=10, m=12, q=7)
+ sage: E
+ BooleanSolve and FXL estimators for the MQ problem
+ """
+ _variants = ("las_vegas", "deterministic")
+
+ def __init__(self, n, m, q, w=2, h=0):
+ if not isinstance(q, (int, Integer)):
+ raise TypeError("q must be an integer")
+ super().__init__(n=n, m=m, q=q, w=w, h=h)
+
+ if self.is_defined_over_finite_field():
+ if not self.npolynomials_reduced() >= self.nvariables_reduced() and not self.npolynomials_reduced() == self.nvariables_reduced():
+ raise ValueError("the no. of polynomials must be >= than the no. of variables")
+ else:
+ if not self.npolynomials_reduced() >= self.nvariables_reduced():
+ raise ValueError("the no. of polynomials must be > than the no. of variables")
+
+ self._k = None
+ self._variant = None
+ self._time_complexity = None
+ self._memory_complexity = None
+ self._compute_optimal_k_ = self._compute_time_complexity_
+ self._compute_optimal_variant_ = self._compute_time_complexity_
+
+ @optimal_parameter
+ def k(self):
+ """
+ Return the optimal `k`
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import BooleanSolveFXL
+ sage: E = BooleanSolveFXL(n=10, m=12, q=7)
+ sage: E.k()
+ 4
+ """
+ if self._k is None:
+ self._compute_optimal_k_()
+ return self._k
+
+ @optimal_parameter
+ def variant(self):
+ """
+ Return the optimal variant
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import BooleanSolveFXL
+ sage: E = BooleanSolveFXL(n=10, m=12, q=7)
+ sage: E.variant()
+ 'deterministic'
+ """
+ if self._variant is None:
+ self._compute_optimal_variant_()
+ return self._variant
+
+ def time_complexity(self, **kwargs):
+ """
+ Return the time complexity of BooleanSolve and FXL algorithms
+
+ INPUT:
+
+ - ``k`` -- the optimal `k` (default: None)
+ - ``variant`` -- the selected variant (default: None)
+
+ If `k` and `variant` are specified, the function returns the time complexity w.r.t the given parameters
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import BooleanSolveFXL
+ sage: E = BooleanSolveFXL(n=10, m=12, q=7)
+ sage: float(log(E.time_complexity(), 2))
+ 27.599017034509096
+ sage: float(log(E.time_complexity(k=2, variant="las_vegas"), 2))
+ 33.35111811760744
+ """
+
+ k = kwargs.get('k', None)
+ variant = kwargs.get('variant', None)
+
+ if k is not None and variant is not None:
+ time_complexity = self._time_complexity_(k, variant)
+ else:
+ self._compute_time_complexity_()
+ time_complexity = self._time_complexity
+
+ h = self._h
+ q = self.order_of_the_field()
+ time_complexity *= q ** h
+ return time_complexity
+
+ def memory_complexity(self):
+ """
+ Return the memory complexity of BooleanSolve and FXL algorithms
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import BooleanSolveFXL
+ sage: E = BooleanSolveFXL(n=10, m=12, q=7)
+ sage: E.memory_complexity()
+ 3136
+ """
+ if self._memory_complexity is None:
+ n, m = self.nvariables_reduced(), self.npolynomials_reduced()
+ q = self.order_of_the_field()
+ k = self.k()
+ wit_deg = witness_degree.quadratic_system(n=n - k, m=m, q=q)
+ if self.variant() == "las_vegas":
+ a = binomial(n - k + 2, 2)
+ T = binomial(n - k + wit_deg - 2, wit_deg)
+ N = binomial(n - k + wit_deg, wit_deg)
+ self._memory_complexity = m * a + (T * a * log(N, 2) + N * log(m, 2)) / log(q, 2)
+ else:
+ self._memory_complexity = max(binomial(n - k + wit_deg - 1, wit_deg) ** 2, m * n ** 2)
+
+ return self._memory_complexity
+
+ def tilde_o_time(self):
+ """
+ Return the Ε time complexity of BooleanSolve and FXL algorithms
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import BooleanSolveFXL
+ sage: E = BooleanSolveFXL(n=10, m=12, q=7)
+ sage: float(log(E.tilde_o_time(), 2))
+ 24.014054533787938
+ """
+ n, m = self.nvariables_reduced(), self.npolynomials_reduced()
+ q = self.order_of_the_field()
+ w = self.linear_algebra_constant()
+ k = self.k()
+ variant = self.variant()
+ wit_deg = witness_degree.quadratic_system(n=n - k, m=m, q=q)
+
+ if n == m and q == 2:
+ return 2 ** (0.792 * m)
+ elif variant == 'las_vegas':
+ complexity = q ** k * binomial(n - k + wit_deg, wit_deg) ** 2
+ else:
+ complexity = q ** k * binomial(n - k + wit_deg, wit_deg) ** w
+
+ h = self._h
+ return q ** h * complexity
+
+ def _compute_time_complexity_(self):
+ min_time_complexity = Infinity
+
+ n, m = self.nvariables_reduced(), self.npolynomials_reduced()
+
+ optimal_k = optimal_variant = None
+
+ for variant in BooleanSolveFXL._variants:
+ a = 0 if self.is_overdefined_system() else 1
+ for k in range(a, n):
+
+ time_complexity = self._time_complexity_(k, variant)
+
+ if time_complexity < min_time_complexity:
+ min_time_complexity = time_complexity
+ optimal_k = k
+ optimal_variant = variant
+
+ self._time_complexity = min_time_complexity
+ self._k = optimal_k
+ self._variant = optimal_variant
+
+ def _time_complexity_(self, k, variant):
+ """
+ Return the time complexity for the given parameter
+
+ INPUT:
+
+ - ``k`` -- the value `k`
+ - ``variant`` -- the variant of the algorithm
+ """
+ n, m = self.nvariables_reduced(), self.npolynomials_reduced()
+ q = self.order_of_the_field()
+ w = self.linear_algebra_constant()
+
+ wit_deg = witness_degree.quadratic_system(n=n - k, m=m, q=q)
+
+ if variant == "las_vegas":
+ time_complexity = 3 * binomial(n - k + 2, 2) * q ** k * binomial(n - k + wit_deg, wit_deg) ** 2
+ elif variant == "deterministic":
+ time_complexity = q ** k * m * binomial(n - k + wit_deg, wit_deg) ** w
+ else:
+ raise ValueError("variant must either be las_vegas or deterministic")
+
+ return time_complexity
+
+ def __repr__(self):
+ return f"BooleanSolve and FXL estimators for the MQ problem"
+
+ # all methods below are implemented to overwrite the parent's docstring while keeping the implementation
+
+ def has_optimal_parameter(self):
+ """
+ Return `True` if the algorithm has optimal parameter
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import BooleanSolveFXL
+ sage: H = BooleanSolveFXL(q=256, n=5, m=10)
+ sage: H.has_optimal_parameter()
+ True
+ """
+ return super().has_optimal_parameter()
+
+ def is_defined_over_finite_field(self):
+ """
+ Return `True` if the algorithm is defined over a finite field
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import BooleanSolveFXL
+ sage: H = BooleanSolveFXL(q=256, n=5, m=10)
+ sage: H.is_defined_over_finite_field()
+ True
+ """
+ return super().is_defined_over_finite_field()
+
+ def is_overdefined_system(self):
+ """
+ Return `True` if the system is overdefined
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import BooleanSolveFXL
+ sage: H = BooleanSolveFXL(q=256, n=5, m=10)
+ sage: H.is_overdefined_system()
+ True
+ sage: E = BooleanSolveFXL(q=256, n=10, m=10)
+ sage: E.is_overdefined_system()
+ False
+ """
+ return super().is_overdefined_system()
+
+ def is_square_system(self):
+ """
+ Return `True` if the system is square, there are equal no. of variables and polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import BooleanSolveFXL
+ sage: H = BooleanSolveFXL(q=256, n=5, m=10)
+ sage: H.is_square_system()
+ False
+ sage: E = BooleanSolveFXL(q=256, n=10, m=10)
+ sage: E.is_square_system()
+ True
+ """
+ return super().is_square_system()
+
+ def is_underdefined_system(self):
+ """
+ Return `True` if the system is underdefined
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import BooleanSolveFXL
+ sage: H = BooleanSolveFXL(q=256, n=5, m=10)
+ sage: H.is_underdefined_system()
+ False
+ """
+ return super().is_underdefined_system()
+
+ def linear_algebra_constant(self):
+ """
+ Return the linear algebra constant
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import BooleanSolveFXL
+ sage: H = BooleanSolveFXL(q=256, n=5, m=10, w=2)
+ sage: H.linear_algebra_constant()
+ 2
+ """
+ return super().linear_algebra_constant()
+
+ def npolynomials(self):
+ """
+ Return the number of polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import BooleanSolveFXL
+ sage: H = BooleanSolveFXL(q=256, n=5, m=10)
+ sage: H.npolynomials()
+ 10
+ """
+ return super().npolynomials()
+
+ def nvariables(self):
+ """
+ Return the number of variables
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import BooleanSolveFXL
+ sage: H = BooleanSolveFXL(q=256, n=10, m=10)
+ sage: H.nvariables()
+ 10
+ """
+ return super().nvariables()
+
+ def nvariables_reduced(self):
+ """
+ Return the no. of variables after fixing some values
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import BooleanSolveFXL
+ sage: H = BooleanSolveFXL(q=256, n=10, m=10)
+ sage: H.nvariables_reduced()
+ 10
+ """
+ return super().nvariables_reduced()
+
+ def npolynomials_reduced(self):
+ """
+ Return the no. of polynomials after applying the Thomae and Wolf strategy
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import BooleanSolveFXL
+ sage: H = BooleanSolveFXL(q=256, n=5, m=10)
+ sage: H.npolynomials_reduced()
+ 10
+ """
+ return super().npolynomials_reduced()
+
+ def optimal_parameters(self):
+ """
+ Return a dictionary of optimal parameters
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import BooleanSolveFXL
+ sage: H = BooleanSolveFXL(q=256, n=10, m=10)
+ sage: H.optimal_parameters()
+ {'k': 2, 'variant': 'deterministic'}
+ """
+ return super().optimal_parameters()
+
+ def order_of_the_field(self):
+ """
+ Return the order of the field
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import BooleanSolveFXL
+ sage: H = BooleanSolveFXL(q=256, n=10, m=10)
+ sage: H.order_of_the_field()
+ 256
+ """
+ return super().order_of_the_field()
\ No newline at end of file
diff --git a/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/cgmta.py b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/cgmta.py
new file mode 100644
index 00000000..dcd77013
--- /dev/null
+++ b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/cgmta.py
@@ -0,0 +1,283 @@
+# *****************************************************************************
+# Multivariate Quadratic (MQ) Estimator
+# Copyright (C) 2021-2022 Emanuele Bellini, Rusydi H. Makarim, Javier Verbel
+# Cryptography Research Centre, Technology Innovation Institute LLC
+#
+# This file is part of MQ Estimator
+#
+# MQ Estimator 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.
+#
+# MQ Estimator 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
+# MQ Estimator. If not, see .
+# *****************************************************************************
+
+
+from sage.all import Integer
+from sage.functions.other import sqrt, floor
+from sage.misc.functional import numerical_approx
+from .base import BaseAlgorithm
+from sage.functions.other import binomial
+
+
+class CGMTA(BaseAlgorithm):
+ r"""
+ Construct an instance of CGMT-A estimator
+
+ CGMT-A is an algorithm to solve the MQ problem over any finite field. It works when there is an integer $k$ such
+ that $m - 2k < 2k^2 \leq n - 2k$ [CGMT02]_.
+
+ NOTE::
+
+ In this module the compleixties are computed
+ for k= min(m / 2, floor(sqrt(n / 2 - sqrt(n / 2)))).
+
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``m`` -- no. of polynomials
+ - ``q`` -- order of the finite field
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import CGMTA
+ sage: E = CGMTA(n=41, m=10, q=3)
+ sage: E
+ CGMT-A estimator for the MQ problem
+
+ TESTS::
+
+ sage: E.nvariables() == E.nvariables_reduced()
+ True
+ """
+ def __init__(self, n, m, q):
+ if not isinstance(q, (int, Integer)):
+ raise TypeError("q must be an integer")
+
+ if not m <= n:
+ raise ValueError("m must be <= n")
+
+ super().__init__(n=n, m=m, q=q)
+ self._k = min(m / 2, floor(sqrt(n / 2 - sqrt(n / 2))))
+
+ if not 2 * self._k ** 2 <= n - 2 * self._k or not m - 2 * self._k < 2 * self._k ** 2:
+ raise ValueError(f'The condition m - 2k < 2k^2 <= n - 2k must be satisfied')
+
+ self._n_reduced = n
+ self._m_reduced = m
+
+ def time_complexity(self):
+ """
+ Return the time complexity of CGMT-A algorithm
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import CGMTA
+ sage: E = CGMTA(n=41, m=10, q=3)
+ sage: float(log(E.time_complexity(),2))
+ 23.137080884841783
+ """
+ n = self.nvariables()
+ m = self.npolynomials()
+ q = self.order_of_the_field()
+ k = self._k
+ return numerical_approx(2 * k * binomial(n - k, 2) * q ** (m - k))
+
+ def memory_complexity(self):
+ """
+ Return the memory complexity of CGMT-A algorithm
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import CGMTA
+ sage: E = CGMTA(n=41, m=10, q=3)
+ sage: E.memory_complexity()
+ 162.000000000000
+ """
+ q = self.order_of_the_field()
+ k = self._k
+ return numerical_approx(2 * k * q ** k)
+
+ def tilde_o_time(self):
+ """
+ Return the Ε time complexity of of CGMT-A algorithm
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import CGMTA
+ sage: E = CGMTA(n=41, m=10, q=3)
+ sage: E.tilde_o_time()
+ 2187.00000000000
+ """
+ m = self.npolynomials()
+ q = self.order_of_the_field()
+ k = self._k
+ return numerical_approx(q ** (m - k))
+
+ def __repr__(self):
+ return f"CGMT-A estimator for the MQ problem"
+
+ # all methods below are implemented to overwrite the parent's docstring while keeping the implementation
+
+ def has_optimal_parameter(self):
+ """
+ Return `True` if the algorithm has optimal parameter
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import CGMTA
+ sage: H = CGMTA(n=41, m=10, q=3)
+ sage: H.has_optimal_parameter()
+ False
+ """
+ return super().has_optimal_parameter()
+
+ def is_defined_over_finite_field(self):
+ """
+ Return `True` if the algorithm is defined over a finite field
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import CGMTA
+ sage: H = CGMTA(n=41, m=10, q=3)
+ sage: H.is_defined_over_finite_field()
+ True
+ """
+ return super().is_defined_over_finite_field()
+
+ def is_overdefined_system(self):
+ """
+ Return `True` if the system is overdefined
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import CGMTA
+ sage: H = CGMTA(n=41, m=10, q=3)
+ sage: H.is_overdefined_system()
+ False
+ """
+ return super().is_overdefined_system()
+
+ def is_square_system(self):
+ """
+ Return `True` if the system is square, there are equal no. of variables and polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import CGMTA
+ sage: H = CGMTA(n=41, m=10, q=3)
+ sage: H.is_square_system()
+ False
+ """
+ return super().is_square_system()
+
+ def is_underdefined_system(self):
+ """
+ Return `True` if the system is underdefined
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import CGMTA
+ sage: E = CGMTA(n=41, m=10, q=3)
+ sage: E.is_underdefined_system()
+ True
+ """
+ return super().is_underdefined_system()
+
+ def linear_algebra_constant(self):
+ """
+ Return the linear algebra constant
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import CGMTA
+ sage: H = CGMTA(n=41, m=10, q=3)
+ sage: H.linear_algebra_constant()
+
+ """
+ return super().linear_algebra_constant()
+
+ def npolynomials(self):
+ """
+ Return the number of polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import CGMTA
+ sage: H = CGMTA(n=41, m=10, q=3)
+ sage: H.npolynomials()
+ 10
+ """
+ return super().npolynomials()
+
+ def nvariables(self):
+ """
+ Return the number of variables
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import CGMTA
+ sage: H = CGMTA(n=41, m=10, q=3)
+ sage: H.nvariables()
+ 41
+ """
+ return super().nvariables()
+
+ def nvariables_reduced(self):
+ """
+ Return the no. of variables after fixing some values
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import CGMTA
+ sage: H = CGMTA(n=41, m=10, q=3)
+ sage: H.nvariables_reduced()
+ 41
+ """
+ return super().nvariables_reduced()
+
+ def npolynomials_reduced(self):
+ """
+ Return the no. of polynomials after applying the Thomae and Wolf strategy
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import CGMTA
+ sage: H = CGMTA(n=41, m=10, q=3)
+ sage: H.npolynomials_reduced()
+ 10
+ """
+ return super().npolynomials_reduced()
+
+ def optimal_parameters(self):
+ """
+ Return a dictionary of optimal parameters
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import CGMTA
+ sage: H = CGMTA(n=41, m=10, q=3)
+ sage: H.optimal_parameters()
+ {}
+ """
+ return super().optimal_parameters()
+
+ def order_of_the_field(self):
+ """
+ Return the order of the field
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import CGMTA
+ sage: H = CGMTA(n=41, m=10, q=3)
+ sage: H.order_of_the_field()
+ 3
+ """
+ return super().order_of_the_field()
\ No newline at end of file
diff --git a/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/crossbred.py b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/crossbred.py
new file mode 100644
index 00000000..1da55345
--- /dev/null
+++ b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/crossbred.py
@@ -0,0 +1,580 @@
+# *****************************************************************************
+# Multivariate Quadratic (MQ) Estimator
+# Copyright (C) 2021-2022 Emanuele Bellini, Rusydi H. Makarim, Javier Verbel
+# Cryptography Research Centre, Technology Innovation Institute LLC
+#
+# This file is part of MQ Estimator
+#
+# MQ Estimator 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.
+#
+# MQ Estimator 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
+# MQ Estimator. If not, see .
+# *****************************************************************************
+
+
+from sage.all import Integer
+from sage.functions.log import log
+from sage.functions.other import binomial
+from sage.rings.all import QQ
+from sage.rings.infinity import Infinity
+from sage.rings.power_series_ring import PowerSeriesRing
+from .base import BaseAlgorithm, optimal_parameter
+from ..series.hilbert import HilbertSeries
+from ..series.nmonomial import NMonomialSeries
+from ..utils import nmonomials_up_to_degree
+
+
+class Crossbred(BaseAlgorithm):
+ r"""
+ Construct an instance of crossbred estimator
+
+ The Crossbred is an algorithm to solve the MQ problem [JV18]_. This algorithm consists of two steps, named the
+ preprocessing step and the linearization step. In the preprocessing step, we find a set $S$ of degree-$D$
+ polynomials in the ideal generated by the initial set of polynomials. Every specialization of the first $n-k$
+ variables of the polynomials in $S$ results in a set $S'$ of degree-$d$ polynomials in $k$ variables. Finally, in
+ the linearization step, a solution to $S'$ is found by direct linearization.
+
+ .. NOTE::
+
+ Our complexity estimates are a generalization over any field of size `q` of the complexity formulas given in
+ [Dua20]_, which are given either for `q=2` or generic fields.
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``m`` -- no. of polynomials
+ - ``q`` -- order of the finite field
+ - ``w`` -- linear algebra constant (2 <= w <= 3) (default: 2)
+ - ``max_D`` -- upper bound to the parameter D (default: 10)
+ - ``h`` -- external hybridization parameter (default: 0)
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: E = Crossbred(n=10, m=12, q=5)
+ sage: E
+ Crossbred estimator for the MQ problem
+ """
+ def __init__(self, n, m, q, w=2, max_D=30, h=0):
+ if not isinstance(q, (int, Integer)):
+ raise TypeError("q must be an integer")
+
+ self._max_D = min(max_D, min(n, m))
+ super().__init__(n=n, m=m, q=q, w=w, h=h)
+ self._k = None
+ self._D = None
+ self._d = None
+ self._time_complexity = None
+ self._memory_complexity = None
+
+ @property
+ def max_D(self):
+ """
+ Return the upper bound of the degree of the initial Macaulay matrix
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: E = Crossbred(n=10, m=12, q=5, max_D=9)
+ sage: E.max_D
+ 9
+ """
+ return self._max_D
+
+ @max_D.setter
+ def max_D(self, value):
+ """
+ Set new upper bound of the degree of the initial Macaulay matrix
+
+ TESTS::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: E = Crossbred(n=10, m=12, q=5, max_D=6)
+ sage: E.admissible_parameter_series(1)
+ -1 - 2*x - 2*y - 2*x*y + 9*y^2 + 65*x^3 + 9*x^2*y + 9*x*y^2 + 20*y^3 + 439*x^4 + 119*x^3*y + 9*x^2*y^2 +
+ 20*x*y^3 - 35*y^4 + 1705*x^5 + 658*x^4*y + 20*x^3*y^2 + 20*x^2*y^3 - 35*x*y^4 - 89*y^5 + 4892*x^6 +
+ 2419*x^5*y + 64*x^4*y^2 + 20*x^3*y^3 - 35*x^2*y^4 - 89*x*y^5 + 77*y^6 + O(x, y)^7
+ sage: E.max_D = 5
+ sage: E.admissible_parameter_series(1)
+ -1 - 2*x - 2*y - 2*x*y + 9*y^2 + 65*x^3 + 9*x^2*y + 9*x*y^2 + 20*y^3 + 439*x^4 + 119*x^3*y + 9*x^2*y^2 +
+ 20*x*y^3 - 35*y^4 + 1705*x^5 + 658*x^4*y + 20*x^3*y^2 + 20*x^2*y^3 - 35*x*y^4 - 89*y^5 + O(x, y)^6
+ """
+ self._max_D = value
+ self._k = None
+ self._D = None
+ self._d = None
+ self._time_complexity = None
+ self._memory_complexity = None
+
+ def ncols_in_preprocessing_step(self, k, D, d):
+ """
+ Return the number of columns involve in the preprocessing step
+
+ INPUT:
+
+ - ``k`` -- no. variables in the resulting system
+ - ``D`` -- degree of the initial Macaulay matrix
+ - ``d`` -- degree resulting Macaulay matrix
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: E = Crossbred(n=10, m=12, q=5)
+ sage: E.ncols_in_preprocessing_step(4, 6, 3)
+ 297
+ """
+ if not d < D:
+ raise ValueError("d must be smaller than D")
+
+ n = self.nvariables_reduced()
+ q = self.order_of_the_field()
+
+ nms0 = NMonomialSeries(n=k, q=q, max_prec=D+1)
+ nms1 = NMonomialSeries(n=n-k, q=q, max_prec=D+1)
+
+ ncols = 0
+ for dk in range(d + 1, D):
+ ncols += sum([nms0.nmonomials_of_degree(dk) * nms1.nmonomials_of_degree(dp) for dp in range(D - dk)])
+
+ return ncols
+
+ def ncols_in_linearization_step(self, k, d):
+ """
+ Return the number of columns involve in the linearization step
+
+ INPUT:
+
+ - ``k`` -- no. variables in the resulting system
+ - ``d`` -- degree resulting Macaulay matrix
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: E = Crossbred(n=10, m=12, q=5)
+ sage: E.ncols_in_linearization_step(4, 3)
+ 35
+ """
+ return nmonomials_up_to_degree(d, k, q=self.order_of_the_field())
+
+ @optimal_parameter
+ def k(self):
+ """
+ Return the optimal `k`, i.e. no. of variables in the resulting system
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: E = Crossbred(n=10, m=12, q=5)
+ sage: E.k()
+ 7
+ """
+ if self._k is None:
+ _ = self.time_complexity()
+ return self._k
+
+ @optimal_parameter
+ def D(self):
+ """
+ Return the optimal `D`, i.e. degree of the initial Macaulay matrix
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: E = Crossbred(n=10, m=12, q=5)
+ sage: E.D()
+ 5
+ """
+ if self._D is None:
+ _ = self.time_complexity()
+ return self._D
+
+ @optimal_parameter
+ def d(self):
+ """
+ Return the optimal `d`, i.e. degree resulting Macaulay matrix
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: E = Crossbred(n=10, m=12, q=5)
+ sage: E.d()
+ 1
+ """
+ if self._d is None:
+ _ = self.time_complexity()
+ return self._d
+
+ def admissible_parameter_series(self, k):
+ """
+ Return a the series $S_k$ of admissible parameters
+
+ INPUT:
+
+ - ``k`` -- no. variables in the resulting system
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: E = Crossbred(n=10, m=12, q=5, max_D=2)
+ sage: E.admissible_parameter_series(2)
+ -1 - 3*x - 3*y - 10*x^2 - 3*x*y + 6*y^2 + O(x, y)^3
+ """
+ n, m = self.nvariables_reduced(), self.npolynomials_reduced()
+ q = self.order_of_the_field()
+ max_D = self.max_D
+
+ R = PowerSeriesRing(QQ, names=['x', 'y'], default_prec=max_D + 1)
+ x, y = R.gens()
+
+ Hk = HilbertSeries(n=k, degrees=[2]*m, q=q)
+ k_y, k_xy = Hk.series(y), Hk.series(x * y)
+
+ Hn = HilbertSeries(n=n, degrees=[2]*m, q=q)
+ n_x = Hn.series(x)
+
+ N = NMonomialSeries(n=n - k, q=q, max_prec=max_D + 1)
+ nk_x = N.series_monomials_of_degree()(x)
+
+ return (k_xy * nk_x - n_x - k_y) / ((1 - x) * (1 - y))
+
+ def admissible_parameters(self):
+ """
+ Return a list of admissible parameters `(k, D, d)`
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: E = Crossbred(n=10, m=12, q=5)
+ sage: E.admissible_parameters()[:5]
+ [(1, 2, 1), (1, 3, 1), (1, 4, 1), (1, 3, 2), (1, 5, 1)]
+ """
+ n = self.nvariables_reduced()
+ max_D = self.max_D
+
+ admissible_parameters = []
+ for k in range(1, n):
+ Sk = self.admissible_parameter_series(k)
+ possibles_D_d = [monomial.exponents()[0]
+ for (monomial, coefficient) in Sk.coefficients().items()
+ if (0 <= coefficient)
+ and (monomial.exponents()[0][0] > monomial.exponents()[0][1])
+ and (monomial.exponents()[0][0] <= max_D)
+ and (1 <= monomial.exponents()[0][1])]
+ admissible_parameters.extend([(k, D, d) for D, d in possibles_D_d])
+
+ return admissible_parameters
+
+ def time_complexity(self, **kwargs):
+ """
+ Return the time complexity
+
+ INPUT:
+
+ - ``k`` -- no. of variables in the resulting system (default: None)
+ - ``D`` -- degree of the initial Macaulay matrix (default: None)
+ - ``d`` -- degree resulting Macaulay matrix (default: None)
+
+ If `k`, `D`, and `d` are specified, the function returns the time complexity w.r.t to the given parameters
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: E = Crossbred(n=10, m=12, q=5)
+ sage: float(log(E.time_complexity(), 2))
+ 19.56992234329735
+ sage: float(log(E.time_complexity(k=4, D=6, d=4), 2))
+ 29.77510134996699
+
+ TESTS::
+
+ sage: E0 = Crossbred(n=15, m=12, q=5)
+ sage: E1 = Crossbred(n=16, m=12, q=5)
+ sage: E0.time_complexity().numerical_approx() == E1.time_complexity().numerical_approx()
+ True
+ """
+ k = kwargs.get('k', None)
+ D = kwargs.get('D', None)
+ d = kwargs.get('d', None)
+
+ h = self._h
+ q = self.order_of_the_field()
+ if all(var is not None for var in (k, D, d)):
+ return q ** h * self._time_complexity_(k, D, d)
+
+ min_time_complexity = Infinity
+ if self._time_complexity is None:
+ for (k, D, d) in self.admissible_parameters():
+ time_complexity = self._time_complexity_(k, D, d)
+ if time_complexity < min_time_complexity:
+ min_time_complexity = time_complexity
+ self._k = k
+ self._D = D
+ self._d = d
+ self._time_complexity = q ** h * min_time_complexity
+
+ return self._time_complexity
+
+ def memory_complexity(self, **kwargs):
+ """
+ Return the memory complexity
+
+ INPUT:
+
+ - ``k`` -- no. of variables in the resulting system (default: None)
+ - ``D`` -- degree of the initial Macaulay matrix (default: None)
+ - ``d`` -- degree resulting Macaulay matrix (default: None)
+
+ If `k`, `D`, and `d` are specified, the function returns the memory complexity w.r.t to the given parameters
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: E = Crossbred(n=10, m=12, q=5)
+ sage: float(log(E.memory_complexity(), 2))
+ 19.380131266596905
+ sage: float(log(E.memory_complexity(k=4, D=6, d=4), 2))
+ 12.892542816648552
+
+ TESTS::
+
+ sage: E0 = Crossbred(n=15, m=12, q=5)
+ sage: E1 = Crossbred(n=16, m=12, q=5)
+ sage: E0.memory_complexity().numerical_approx() == E1.memory_complexity().numerical_approx()
+ True
+ """
+
+ k = kwargs.get('k', None)
+ D = kwargs.get('D', None)
+ d = kwargs.get('d', None)
+
+ if all(var is not None for var in (k, D, d)):
+ ncols_pre_step = self.ncols_in_preprocessing_step(k, D, d)
+ ncols_lin_step = self.ncols_in_linearization_step(k, d)
+ return ncols_pre_step ** 2 + ncols_lin_step ** 2
+
+ if self._memory_complexity is None:
+ D = self.D()
+ k = self.k()
+ d = self.d()
+ ncols_pre_step = self.ncols_in_preprocessing_step(k, D, d)
+ ncols_lin_step = self.ncols_in_linearization_step(k, d)
+ self._memory_complexity = ncols_pre_step ** 2 + ncols_lin_step ** 2
+
+ return self._memory_complexity
+
+ def tilde_o_time(self):
+ """
+ Return the Ε time complexity of crossbred algorithm
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: E = Crossbred(n=10, m=12, q=5)
+ sage: float(log(E.tilde_o_time(), 2))
+ 19.396813798959137
+ """
+ k, D, d = self.k(), self.D(), self.d()
+ np = self.ncols_in_preprocessing_step(k=k, D=D, d=d)
+ nl = self.ncols_in_linearization_step(k=k, d=d)
+ q = self.order_of_the_field()
+ n = self.nvariables_reduced()
+ w = self.linear_algebra_constant()
+ h = self._h
+ return q ** h * np ** 2 + q ** (n - k) * nl ** w
+
+ def _time_complexity_(self, k, D, d):
+ n, m = self.nvariables_reduced(), self.npolynomials_reduced()
+ w = self.linear_algebra_constant()
+ q = self.order_of_the_field()
+ np = self.ncols_in_preprocessing_step(k=k, D=D, d=d)
+ nl = self.ncols_in_linearization_step(k=k, d=d)
+ complexity_wiedemann = 3 * binomial(k + d, d) * binomial(n + 2, 2) * np ** 2
+ complexity_gaussian = np ** w
+ complexity = Infinity
+
+ if np > 1 and log(np, 2) > 1:
+ complexity = min(complexity_gaussian, complexity_wiedemann) + (m * q ** (n - k) * nl ** w)
+ return complexity
+
+ def __repr__(self):
+ return "Crossbred estimator for the MQ problem"
+
+ # all methods below are implemented to overwrite the parent's docstring while keeping the implementation
+
+ def has_optimal_parameter(self):
+ """
+ Return `True` if the algorithm has optimal parameter
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: H = Crossbred(q=256, n=5, m=10)
+ sage: H.has_optimal_parameter()
+ True
+ """
+ return super().has_optimal_parameter()
+
+ def is_defined_over_finite_field(self):
+ """
+ Return `True` if the algorithm is defined over a finite field
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: H = Crossbred(q=256, n=5, m=10)
+ sage: H.is_defined_over_finite_field()
+ True
+ """
+ return super().is_defined_over_finite_field()
+
+ def is_overdefined_system(self):
+ """
+ Return `True` if the system is overdefined
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: H = Crossbred(q=256, n=5, m=10)
+ sage: H.is_overdefined_system()
+ True
+ sage: E = Crossbred(q=256, n=10, m=10)
+ sage: E.is_overdefined_system()
+ False
+ """
+ return super().is_overdefined_system()
+
+ def is_square_system(self):
+ """
+ Return `True` if the system is square, there are equal no. of variables and polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: H = Crossbred(q=256, n=5, m=10)
+ sage: H.is_square_system()
+ False
+ sage: E = Crossbred(q=256, n=10, m=10)
+ sage: E.is_square_system()
+ True
+ """
+ return super().is_square_system()
+
+ def is_underdefined_system(self):
+ """
+ Return `True` if the system is underdefined
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: H = Crossbred(q=256, n=5, m=10)
+ sage: H.is_underdefined_system()
+ False
+ sage: E = Crossbred(q=256, n=10, m=5)
+ sage: E.is_underdefined_system()
+ True
+ """
+ return super().is_underdefined_system()
+
+ def linear_algebra_constant(self):
+ """
+ Return the linear algebra constant
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: H = Crossbred(q=256, n=5, m=10, w=2)
+ sage: H.linear_algebra_constant()
+ 2
+ """
+ return super().linear_algebra_constant()
+
+ def npolynomials(self):
+ """
+ Return the number of polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: H = Crossbred(q=256, n=5, m=10)
+ sage: H.npolynomials()
+ 10
+ """
+ return super().npolynomials()
+
+ def nvariables(self):
+ """
+ Return the number of variables
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: H = Crossbred(q=256, n=5, m=10)
+ sage: H.nvariables()
+ 5
+ """
+ return super().nvariables()
+
+ def nvariables_reduced(self):
+ """
+ Return the no. of variables after fixing some values
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: H = Crossbred(q=256, n=5, m=10)
+ sage: H.nvariables_reduced()
+ 5
+ sage: E = Crossbred(q=256, n=12, m=10)
+ sage: E.nvariables_reduced()
+ 10
+ """
+ return super().nvariables_reduced()
+
+ def npolynomials_reduced(self):
+ """
+ Return the no. of polynomials after applying the Thomae and Wolf strategy
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: H = Crossbred(q=256, n=5, m=10)
+ sage: H.npolynomials_reduced()
+ 10
+ sage: E = Crossbred(q=256, n=12, m=10)
+ sage: E.npolynomials_reduced()
+ 10
+ """
+ return super().npolynomials_reduced()
+
+ def optimal_parameters(self):
+ """
+ Return a dictionary of optimal parameters
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: H = Crossbred(q=256, n=15, m=10)
+ sage: H.optimal_parameters()
+ {'D': 8, 'd': 2, 'k': 8}
+ """
+ return super().optimal_parameters()
+
+ def order_of_the_field(self):
+ """
+ Return the order of the field
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Crossbred
+ sage: H = Crossbred(q=256, n=15, m=10)
+ sage: H.order_of_the_field()
+ 256
+ """
+ return super().order_of_the_field()
\ No newline at end of file
diff --git a/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/dinur1.py b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/dinur1.py
new file mode 100644
index 00000000..e302c60c
--- /dev/null
+++ b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/dinur1.py
@@ -0,0 +1,407 @@
+# *****************************************************************************
+# Multivariate Quadratic (MQ) Estimator
+# Copyright (C) 2021-2022 Emanuele Bellini, Rusydi H. Makarim, Javier Verbel
+# Cryptography Research Centre, Technology Innovation Institute LLC
+#
+# This file is part of MQ Estimator
+#
+# MQ Estimator 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.
+#
+# MQ Estimator 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
+# MQ Estimator. If not, see .
+# *****************************************************************************
+
+
+from sage.functions.log import log
+from sage.functions.other import floor
+from sage.rings.infinity import Infinity
+from .base import BaseAlgorithm, optimal_parameter
+from ..utils import sum_of_binomial_coefficients
+
+
+class DinurFirst(BaseAlgorithm):
+ r"""
+ Construct an instance of Dinur's first estimator
+
+ The Dinur's first is a probabilistic algorithm to solve the MQ problem over GF(2) [Din21a]_. It computes the parity
+ of the number of solutions of many quadratic polynomial systems. These systems come from the specialization, in the
+ original system, of the values in a fixed set of variables.
+
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``m`` -- no. of polynomials
+ - ``nsolutions`` -- number of solutions (default: 1)
+ - ``h`` -- external hybridization parameter (default: 0)
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurFirst
+ sage: E = DinurFirst(n=10, m=12)
+ sage: E
+ Dinur's first estimator for the MQ problem
+ """
+ def __init__(self, n, m, nsolutions=1, h=0):
+ super().__init__(n=n, m=m, q=2, h=h)
+ self._nsolutions = nsolutions
+
+ self._k = floor(log(nsolutions + 1, 2))
+ self._kappa = None
+ self._lambda = None
+ self._time_complexity = None
+ self._memory_complexity = None
+
+ @optimal_parameter
+ def Ξ»(self):
+ r"""
+ Return the optimal `\lambda`
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurFirst
+ sage: E = DinurFirst(n=10, m=12)
+ sage: E.Ξ»()
+ 1/9
+ """
+ if self._lambda is None:
+ self._compute_kappa_and_lambda_()
+ return self._lambda
+
+ @optimal_parameter
+ def ΞΊ(self):
+ r"""
+ Return the optimal `\kappa`
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurFirst
+ sage: E = DinurFirst(n=10, m=12)
+ sage: E.ΞΊ()
+ 2/9
+ """
+ if self._kappa is None:
+ self._compute_kappa_and_lambda_()
+ return self._kappa
+
+ def time_complexity(self, **kwargs):
+ r"""
+ Return the time complexity of Dinur's first algorithm
+
+ INPUT:
+
+ - ``ΞΊ`` -- the parameter `\kappa` (kappa)
+ - ``Ξ»`` -- the parameter `\lambda`
+
+ If `\kappa` and `\lambda` are specified, the function returns the time complexity w.r.t. the given parameter
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurFirst
+ sage: E = DinurFirst(n=10, m=12)
+ sage: float(log(E.time_complexity(), 2))
+ 26.819919688075288
+ sage: float(log(E.time_complexity(ΞΊ=0.9, Ξ»=0.9), 2))
+ 16.73237302312492
+
+ TESTS::
+
+ sage: E0 = DinurFirst(n=15, m=12)
+ sage: E1 = DinurFirst(n=17, m=12)
+ sage: E0.time_complexity().numerical_approx() == E1.time_complexity().numerical_approx()
+ True
+ """
+ n = self.nvariables_reduced()
+ k = self._k
+ lambda_ = kwargs.get('Ξ»', self.Ξ»())
+ kappa = kwargs.get('ΞΊ', self.ΞΊ())
+
+ def w(i, kappa):
+ return floor((n - i) * (1 - kappa))
+
+ def n1(i, kappa):
+ return floor((n - i) * kappa)
+
+ if lambda_ == self.Ξ»() and kappa == self.ΞΊ():
+ if self._time_complexity is None:
+ self._time_complexity = 8 * k * log(n, 2) * \
+ sum([self._T(n - i, n1(i, kappa), w(i, kappa), lambda_) for i in range(1, n)])
+ time_complexity = self._time_complexity
+ else:
+ time_complexity = 8 * k * log(n, 2) * \
+ sum([self._T(n - i, n1(i, kappa), w(i, kappa), lambda_) for i in range(1, n)])
+
+ h = self._h
+ time_complexity *= 2 ** h
+ return time_complexity
+
+ def memory_complexity(self):
+ """
+ Return the memory complexity of Dinur's first algorithm
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurFirst
+ sage: E = DinurFirst(n=10, m=12)
+ sage: float(log(E.memory_complexity(), 2))
+ 15.909893083770042
+
+ TESTS::
+
+ sage: E0 = DinurFirst(n=15, m=12)
+ sage: E1 = DinurFirst(n=17, m=12)
+ sage: E0.memory_complexity().numerical_approx() == E1.memory_complexity().numerical_approx()
+ True
+ """
+ if self._memory_complexity is None:
+ kappa = self.ΞΊ()
+ n = self.nvariables_reduced()
+ self._memory_complexity = (48 * n + 1) * 2 ** (floor((1 - kappa) * n))
+
+ return self._memory_complexity
+
+ def _time_complexity_(self, kappa, lambda_):
+ k = self._k
+ n = self.nvariables_reduced()
+ def w(i, kappa):
+ return floor((n - i) * (1 - kappa))
+
+ def n1(i, kappa):
+ return floor((n - i) * kappa)
+
+ return 8 * k * log(n, 2) * sum([self._T(n - i, n1(i, kappa),
+ w(i, kappa), lambda_) for i in range(1, n)])
+
+ def _compute_kappa_and_lambda_(self):
+ min_complexity = Infinity
+ n, m = self.nvariables_reduced(), self.npolynomials_reduced()
+ k = self._k
+ optimal_kappa = None
+ optimal_lambda = None
+
+ for n1 in range(1, min(m + k, (n - 1) // 3) + 1):
+ kappa = n1 / (n - 1)
+ for n2 in range(1, n1):
+ lambda_ = (n1 - n2) / (n - 1)
+ complexity = self._time_complexity_(kappa, lambda_)
+ if complexity < min_complexity:
+ min_complexity = complexity
+ optimal_kappa = kappa
+ optimal_lambda = lambda_
+
+ self._kappa = optimal_kappa
+ self._lambda = optimal_lambda
+
+ def tilde_o_time(self):
+ r"""
+ Return the `\widetilde{O}` time complexity of Dinur's first algorithm
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurFirst
+ sage: E = DinurFirst(n=10, m=12)
+ sage: float(log(E.tilde_o_time(), 2))
+ 6.943
+ """
+ n = self.nvariables_reduced()
+ h = self._h
+ return 2 ** h * 2 ** (0.6943 * n)
+
+ def _T(self, n, n1, w, lambda_):
+ t = 48 * n + 1
+ n2 = floor(n1 - lambda_ * n)
+ l = n2 + 2
+ m = self.npolynomials_reduced()
+ k = self._k
+
+ if n2 <= 0:
+ return n * sum_of_binomial_coefficients(n - n1, w) * 2 ** n1
+ else:
+ temp1 = self._T(n, n2, n2 + 4, lambda_)
+ temp2 = n * sum_of_binomial_coefficients(n - n1, w) * 2 ** (n1 - n2)
+ temp3 = n * sum_of_binomial_coefficients(n - n2, n2 + 4)
+ temp4 = l * (m + k + 2) * sum_of_binomial_coefficients(n, 2)
+ return t * (temp1 + temp2 + temp3 + temp4)
+
+ def __repr__(self):
+ return f"Dinur's first estimator for the MQ problem"
+
+ # all methods below are implemented to overwrite the parent's docstring while keeping the implementation
+
+ def has_optimal_parameter(self):
+ """
+ Return `True` if the algorithm has optimal parameter
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurFirst
+ sage: H = DinurFirst(n=5, m=10)
+ sage: H.has_optimal_parameter()
+ True
+ """
+ return super().has_optimal_parameter()
+
+ def is_defined_over_finite_field(self):
+ """
+ Return `True` if the algorithm is defined over a finite field
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurFirst
+ sage: H = DinurFirst(n=5, m=10)
+ sage: H.is_defined_over_finite_field()
+ True
+ """
+ return super().is_defined_over_finite_field()
+
+ def is_overdefined_system(self):
+ """
+ Return `True` if the system is overdefined
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurFirst
+ sage: H = DinurFirst(n=5, m=10)
+ sage: H.is_overdefined_system()
+ True
+ sage: E = DinurFirst(n=10, m=10)
+ sage: E.is_overdefined_system()
+ False
+ """
+ return super().is_overdefined_system()
+
+ def is_square_system(self):
+ """
+ Return `True` if the system is square, there are equal no. of variables and polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurFirst
+ sage: H = DinurFirst(n=5, m=10)
+ sage: H.is_square_system()
+ False
+ sage: E = DinurFirst(n=10, m=10)
+ sage: E.is_square_system()
+ True
+ """
+ return super().is_square_system()
+
+ def is_underdefined_system(self):
+ """
+ Return `True` if the system is underdefined
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurFirst
+ sage: H = DinurFirst(n=5, m=10)
+ sage: H.is_underdefined_system()
+ False
+ sage: E = DinurFirst(n=10, m=5)
+ sage: E.is_underdefined_system()
+ True
+ """
+ return super().is_underdefined_system()
+
+ def linear_algebra_constant(self):
+ """
+ Return the linear algebra constant
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurFirst
+ sage: H = DinurFirst(n=5, m=10)
+ sage: H.linear_algebra_constant()
+
+ """
+ return super().linear_algebra_constant()
+
+ def npolynomials(self):
+ """
+ Return the number of polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurFirst
+ sage: H = DinurFirst(n=5, m=10)
+ sage: H.npolynomials()
+ 10
+ """
+ return super().npolynomials()
+
+ def nvariables(self):
+ """
+ Return the number of variables
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurFirst
+ sage: H = DinurFirst(n=5, m=10)
+ sage: H.nvariables()
+ 5
+ """
+ return super().nvariables()
+
+ def nvariables_reduced(self):
+ """
+ Return the no. of variables after fixing some values
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurFirst
+ sage: H = DinurFirst(n=5, m=10)
+ sage: H.nvariables_reduced()
+ 5
+ sage: E = DinurFirst(n=12, m=10)
+ sage: E.nvariables_reduced()
+ 10
+ """
+ return super().nvariables_reduced()
+
+ def npolynomials_reduced(self):
+ """
+ Return the no. of polynomials after applying the Thomae and Wolf strategy
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurFirst
+ sage: H = DinurFirst(n=5, m=10)
+ sage: H.npolynomials_reduced()
+ 10
+ sage: E = DinurFirst(n=12, m=10)
+ sage: E.npolynomials_reduced()
+ 10
+ """
+ return super().npolynomials_reduced()
+
+ def optimal_parameters(self):
+ """
+ Return a dictionary of optimal parameters
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurFirst
+ sage: H = DinurFirst(n=15, m=15)
+ sage: H.optimal_parameters()
+ {'ΞΊ': 1/7, 'Ξ»': 1/14}
+ """
+ return super().optimal_parameters()
+
+ def order_of_the_field(self):
+ """
+ Return the order of the field
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurFirst
+ sage: H = DinurFirst(n=15, m=10)
+ sage: H.order_of_the_field()
+ 2
+ """
+ return super().order_of_the_field()
diff --git a/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/dinur2.py b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/dinur2.py
new file mode 100644
index 00000000..79a3ba62
--- /dev/null
+++ b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/dinur2.py
@@ -0,0 +1,360 @@
+# *****************************************************************************
+# Multivariate Quadratic (MQ) Estimator
+# Copyright (C) 2021-2022 Emanuele Bellini, Rusydi H. Makarim, Javier Verbel
+# Cryptography Research Centre, Technology Innovation Institute LLC
+#
+# This file is part of MQ Estimator
+#
+# MQ Estimator 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.
+#
+# MQ Estimator 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
+# MQ Estimator. If not, see .
+# *****************************************************************************
+
+
+from sage.functions.log import log
+from sage.rings.infinity import Infinity
+from ..utils import sum_of_binomial_coefficients
+from .base import BaseAlgorithm, optimal_parameter
+
+
+class DinurSecond(BaseAlgorithm):
+ """
+ Construct an instance of Dinur's second estimator
+
+ Dinur's second is a probabilistic algorithm to solve the MQ problem over GF(2) [Din21b]_. It is based on ideas from
+ [Din21a]_.
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``m`` -- no. of polynomials
+ - ``h`` -- external hybridization parameter (default: 0)
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurSecond
+ sage: E = DinurSecond(n=10, m=12)
+ sage: E
+ Dinur's second estimator for the MQ problem
+ """
+ def __init__(self, n, m, h=0):
+ super().__init__(n=n, m=m, q=2, h=h)
+ self._n1 = None
+ self._time_complexity = None
+ self._memory_complexity = None
+
+ @optimal_parameter
+ def n1(self):
+ """
+ Return the optimal parameter $n_1$
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurSecond
+ sage: E = DinurSecond(n=10, m=12)
+ sage: E.n1()
+ 4
+ """
+ if self._n1 is None:
+ self._compute_time_complexity_()
+ return self._n1
+
+ def time_complexity(self, **kwargs):
+ """
+ Return the time complexity of the Dinur's second algorithm
+
+ INPUT:
+
+ - ``n1`` -- the parameter $n_1$ (default: None)
+
+ If $n_1$ is provided, the function returns the time complexity w.r.t. the given parameter
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurSecond
+ sage: E = DinurSecond(n=10, m=12)
+ sage: E.time_complexity().numerical_approx()
+ 57434.4699066345
+ sage: E.time_complexity(n1=2).numerical_approx()
+ 58848.1441779413
+
+ TESTS::
+
+ sage: E0 = DinurSecond(n=15, m=12)
+ sage: E1 = DinurSecond(n=17, m=12)
+ sage: E0.time_complexity().numerical_approx() == E1.time_complexity().numerical_approx()
+ True
+ """
+
+ n1 = kwargs.get("n1", None)
+
+ if n1 is not None:
+ time_complexity = self._time_complexity_(n1)
+ else:
+ if self._time_complexity is None:
+ self._compute_time_complexity_()
+ time_complexity = self._time_complexity
+
+ h = self._h
+ time_complexity *= 2 ** h
+ return time_complexity
+
+ def memory_complexity(self, **kwargs):
+ """
+ Return the memory complexity of the Dinur's second algorithm
+
+ INPUT:
+
+ - ``n1`` -- the parameter $n_1$ (default: None)
+
+ If $n_1$ is provided, the function returns the memory complexity w.r.t. the given parameter
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurSecond
+ sage: E = DinurSecond(n=10, m=12)
+ sage: E.memory_complexity()
+ 2560
+ sage: E.memory_complexity(n1=2)
+ 5256
+ """
+ n = self.nvariables_reduced()
+ n1 = kwargs.get("n1", None)
+
+ if n1 is not None:
+ return 8 * (n1 + 1) * sum_of_binomial_coefficients(n - n1, n1 + 3)
+
+ if self._memory_complexity is not None:
+ return self._memory_complexity
+
+ n1 = self.n1()
+ self._memory_complexity = 8 * (n1 + 1) * sum_of_binomial_coefficients(n - n1, n1 + 3)
+
+ return self._memory_complexity
+
+ def _compute_time_complexity_(self):
+ n, m = self.nvariables_reduced(), self.npolynomials_reduced()
+ max_n1 = ((m - 2) // 2) - 1
+ min_time_complexity = Infinity
+ optimal_n1 = None
+
+ for n1 in range(1, max_n1 + 1):
+ time_complexity = self._time_complexity_(n1)
+ if time_complexity < min_time_complexity:
+ optimal_n1 = n1
+ min_time_complexity = time_complexity
+
+ self._n1 = optimal_n1
+ self._time_complexity = min_time_complexity
+
+ def _time_complexity_(self, n1):
+ """
+ Return the time complexity for the given parameter
+
+ INPUT:
+
+ - ``n1`` -- the parameter $n_1$
+ """
+ n = self.nvariables_reduced()
+
+ return 16 * log(n, 2) * 2 ** n1 * sum_of_binomial_coefficients(n - n1, n1 + 3) + \
+ n1 * n * 2 ** (n - n1) + \
+ 2 ** (n - 2 * n1 + 1) * sum_of_binomial_coefficients(n, 2)
+
+ def tilde_o_time(self):
+ """
+ Return the Ε time complexity of dinur's second algorithm
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms.dinur2 import DinurSecond
+ sage: E = DinurSecond(n=10, m=12)
+ sage: E.tilde_o_time()
+ 283.68541077888506
+ """
+ n = self.nvariables_reduced()
+ h = self._h
+ return 2 ** h * 2 ** ((1 - 1./(2.7*2)) * n)
+
+ def __repr__(self):
+ return f"Dinur's second estimator for the MQ problem"
+
+ # all methods below are implemented to overwrite the parent's docstring while keeping the implementation
+
+ def has_optimal_parameter(self):
+ """
+ Return `True` if the algorithm has optimal parameter
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurSecond
+ sage: H = DinurSecond(n=5, m=10)
+ sage: H.has_optimal_parameter()
+ True
+ """
+ return super().has_optimal_parameter()
+
+ def is_defined_over_finite_field(self):
+ """
+ Return `True` if the algorithm is defined over a finite field
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurSecond
+ sage: H = DinurSecond(n=5, m=10)
+ sage: H.is_defined_over_finite_field()
+ True
+ """
+ return super().is_defined_over_finite_field()
+
+ def is_overdefined_system(self):
+ """
+ Return `True` if the system is overdefined
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurSecond
+ sage: H = DinurSecond(n=5, m=10)
+ sage: H.is_overdefined_system()
+ True
+ sage: E = DinurSecond(n=10, m=10)
+ sage: E.is_overdefined_system()
+ False
+ """
+ return super().is_overdefined_system()
+
+ def is_square_system(self):
+ """
+ Return `True` if the system is square, there are equal no. of variables and polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurSecond
+ sage: H = DinurSecond(n=5, m=10)
+ sage: H.is_square_system()
+ False
+ sage: E = DinurSecond(n=10, m=10)
+ sage: E.is_square_system()
+ True
+ """
+ return super().is_square_system()
+
+ def is_underdefined_system(self):
+ """
+ Return `True` if the system is underdefined
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurSecond
+ sage: H = DinurSecond(n=5, m=10)
+ sage: H.is_underdefined_system()
+ False
+ sage: E = DinurSecond(n=10, m=5)
+ sage: E.is_underdefined_system()
+ True
+ """
+ return super().is_underdefined_system()
+
+ def linear_algebra_constant(self):
+ """
+ Return the linear algebra constant
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurSecond
+ sage: H = DinurSecond(n=5, m=10)
+ sage: H.linear_algebra_constant()
+
+ """
+
+ def npolynomials(self):
+ """
+ Return the number of polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurSecond
+ sage: H = DinurSecond(n=5, m=10)
+ sage: H.npolynomials()
+ 10
+ """
+ return super().npolynomials()
+
+ def nvariables(self):
+ """
+ Return the number of variables
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurSecond
+ sage: H = DinurSecond(n=5, m=10)
+ sage: H.nvariables()
+ 5
+ """
+ return super().nvariables()
+
+ def nvariables_reduced(self):
+ """
+ Return the no. of variables after fixing some values
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurSecond
+ sage: H = DinurSecond(n=5, m=10)
+ sage: H.nvariables_reduced()
+ 5
+ sage: E = DinurSecond(n=12, m=10)
+ sage: E.nvariables_reduced()
+ 10
+ """
+ return super().nvariables_reduced()
+
+ def npolynomials_reduced(self):
+ """
+ Return the no. of polynomials after applying the Thomae and Wolf strategy
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurSecond
+ sage: H = DinurSecond(n=5, m=10)
+ sage: H.npolynomials_reduced()
+ 10
+ sage: E = DinurSecond(n=12, m=10)
+ sage: E.npolynomials_reduced()
+ 10
+ """
+ return super().npolynomials_reduced()
+
+ def optimal_parameters(self):
+ """
+ Return a dictionary of optimal parameters
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurSecond
+ sage: H = DinurSecond(n=15, m=10)
+ sage: H.optimal_parameters()
+ {'n1': 2}
+ """
+ return super().optimal_parameters()
+
+ def order_of_the_field(self):
+ """
+ Return the order of the field
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import DinurSecond
+ sage: H = DinurSecond(n=15, m=10)
+ sage: H.order_of_the_field()
+ 2
+ """
+ return super().order_of_the_field()
diff --git a/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/exhaustive_search.py b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/exhaustive_search.py
new file mode 100644
index 00000000..bf49b653
--- /dev/null
+++ b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/exhaustive_search.py
@@ -0,0 +1,313 @@
+# *****************************************************************************
+# Multivariate Quadratic (MQ) Estimator
+# Copyright (C) 2021-2022 Emanuele Bellini, Rusydi H. Makarim, Javier Verbel
+# Cryptography Research Centre, Technology Innovation Institute LLC
+#
+# This file is part of MQ Estimator
+#
+# MQ Estimator 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.
+#
+# MQ Estimator 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
+# MQ Estimator. If not, see .
+# *****************************************************************************
+
+
+"""
+Module to compute the time and memory complexity of the algorithm Exhaustive Search
+
+The Exhaustive Search is an algorithm to solve the MQ problem
+
+[BCC+10] Bouillaguet, C., Chen, H., Cheng, C., Chou, T., Niederhagen, R., Shamir, A., and Yang, B.
+Fast exhaustive search for polynomial systems in F2. In Cryptographic Hardware andEmbedded Systems,
+CHES 2010, 12th International Workshop, Santa Barbara, CA, USA,August 17-20, 2010. Proceedings, pages 203β218, 2010.
+"""
+
+
+
+from sage.all import Integer
+from sage.functions.log import log
+from sage.misc.functional import numerical_approx
+from .base import BaseAlgorithm
+
+
+class ExhaustiveSearch(BaseAlgorithm):
+ r"""
+ Construct an instance of Exhaustive Search estimator
+
+ ExhaustiveSearch solves the MQ problem by evaluating all possible solutions until one is found.
+ The formulas used in this module are generalizations of one shown in [BCCCNSY10]_
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``m`` -- no. of polynomials
+ - ``q`` -- order of the finite field
+ - ``nsolutions`` -- number of solutions (default: 1)
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import ExhaustiveSearch
+ sage: E = ExhaustiveSearch(q=3, n=10, m=12)
+ sage: E
+ Exhaustive search estimator for the MQ problem
+ """
+ def __init__(self, n, m, q, nsolutions=1):
+ if not isinstance(q, (int, Integer)):
+ raise TypeError("q must be an integer")
+
+ super().__init__(n=n, m=m, q=q)
+ self._nsolutions = nsolutions
+
+ def nsolutions(self):
+ """
+ Return the number of solutions
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import ExhaustiveSearch
+ sage: E = ExhaustiveSearch(q=3, n=10, m=12, nsolutions=3)
+ sage: E.nsolutions()
+ 3
+ """
+ return self._nsolutions
+
+ def time_complexity(self):
+ """
+ Return the time complexity of the exhaustive search algorithm
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import ExhaustiveSearch
+ sage: E = ExhaustiveSearch(q=3, n=10, m=12)
+ sage: E.time_complexity()
+ 61880.4962217569
+
+ TESTS::
+
+ sage: E0 = ExhaustiveSearch(n=15, m=12, q=3)
+ sage: E1 = ExhaustiveSearch(n=17, m=12, q=3)
+ sage: E0.time_complexity() == E1.time_complexity()
+ True
+ """
+ n = self.nvariables_reduced()
+ nsolutions = self.nsolutions()
+ q = self.order_of_the_field()
+ if q == 2:
+ complexity = 4 * log(n, 2) * (2 ** n / (nsolutions + 1))
+ else:
+ complexity = log(n, q) * (q ** n / (nsolutions + 1))
+
+ return numerical_approx(complexity)
+
+ def memory_complexity(self):
+ """
+ Return the memory complexity of the exhaustive search algorithm
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import ExhaustiveSearch
+ sage: E = ExhaustiveSearch(q=3, n=10, m=12)
+ sage: E.memory_complexity()
+ 1200
+
+ TESTS::
+
+ sage: E0 = ExhaustiveSearch(n=15, m=12, q=3)
+ sage: E1 = ExhaustiveSearch(n=17, m=12, q=3)
+ sage: E0.memory_complexity() == E1.memory_complexity()
+ True
+ """
+ n, m = self.nvariables_reduced(), self.npolynomials_reduced()
+ return m * n ** 2
+
+ def tilde_o_time(self):
+ """
+ Return the Ε time complexity of the exhaustive search algorithm
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import ExhaustiveSearch
+ sage: E = ExhaustiveSearch(q=3, n=10, m=12)
+ sage: E.tilde_o_time()
+ 59049
+ """
+ q = self.order_of_the_field()
+ n = self.nvariables_reduced()
+ return q ** n
+
+ def __repr__(self):
+ return f"Exhaustive search estimator for the MQ problem"
+
+ # all methods below are implemented to overwrite the parent's docstring while keeping the implementation
+
+ def has_optimal_parameter(self):
+ """
+ Return `True` if the algorithm has optimal parameter
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import ExhaustiveSearch
+ sage: H = ExhaustiveSearch(q=256, n=5, m=10)
+ sage: H.has_optimal_parameter()
+ False
+ """
+ return super().has_optimal_parameter()
+
+ def is_defined_over_finite_field(self):
+ """
+ Return `True` if the algorithm is defined over a finite field
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import ExhaustiveSearch
+ sage: H = ExhaustiveSearch(q=256, n=5, m=10)
+ sage: H.is_defined_over_finite_field()
+ True
+ """
+ return super().is_defined_over_finite_field()
+
+ def is_overdefined_system(self):
+ """
+ Return `True` if the system is overdefined
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import ExhaustiveSearch
+ sage: H = ExhaustiveSearch(q=256, n=5, m=10)
+ sage: H.is_overdefined_system()
+ True
+ sage: E = ExhaustiveSearch(q=256, n=10, m=10)
+ sage: E.is_overdefined_system()
+ False
+ """
+ return super().is_overdefined_system()
+
+ def is_square_system(self):
+ """
+ Return `True` if the system is square, there are equal no. of variables and polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import ExhaustiveSearch
+ sage: H = ExhaustiveSearch(q=256, n=5, m=10)
+ sage: H.is_square_system()
+ False
+ sage: E = ExhaustiveSearch(q=256, n=10, m=10)
+ sage: E.is_square_system()
+ True
+ """
+ return super().is_square_system()
+
+ def is_underdefined_system(self):
+ """
+ Return `True` if the system is underdefined
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import ExhaustiveSearch
+ sage: H = ExhaustiveSearch(q=256, n=5, m=10)
+ sage: H.is_underdefined_system()
+ False
+ sage: E = ExhaustiveSearch(q=256, n=10, m=5)
+ sage: E.is_underdefined_system()
+ True
+ """
+ return super().is_underdefined_system()
+
+ def linear_algebra_constant(self):
+ """
+ Return the linear algebra constant
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import ExhaustiveSearch
+ sage: H = ExhaustiveSearch(q=256, n=5, m=10)
+ sage: H.linear_algebra_constant()
+
+ """
+ return super().linear_algebra_constant()
+
+ def npolynomials(self):
+ """
+ Return the number of polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import ExhaustiveSearch
+ sage: H = ExhaustiveSearch(q=256, n=5, m=10)
+ sage: H.npolynomials()
+ 10
+ """
+ return super().npolynomials()
+
+ def nvariables(self):
+ """
+ Return the number of variables
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import ExhaustiveSearch
+ sage: H = ExhaustiveSearch(q=256, n=10, m=10)
+ sage: H.nvariables()
+ 10
+ """
+ return super().nvariables()
+
+ def nvariables_reduced(self):
+ """
+ Return the no. of variables after fixing some values
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import ExhaustiveSearch
+ sage: H = ExhaustiveSearch(q=256, n=10, m=10)
+ sage: H.nvariables_reduced()
+ 10
+ """
+ return super().nvariables_reduced()
+
+ def npolynomials_reduced(self):
+ """
+ Return the no. of polynomials after applying the Thomae and Wolf strategy
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import ExhaustiveSearch
+ sage: H = ExhaustiveSearch(q=256, n=10, m=10)
+ sage: H.npolynomials_reduced()
+ 10
+ """
+ return super().npolynomials_reduced()
+
+ def optimal_parameters(self):
+ """
+ Return a dictionary of optimal parameters
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import ExhaustiveSearch
+ sage: H = ExhaustiveSearch(q=256, n=10, m=10)
+ sage: H.optimal_parameters()
+ {}
+ """
+ return super().optimal_parameters()
+
+ def order_of_the_field(self):
+ """
+ Return the order of the field
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import ExhaustiveSearch
+ sage: H = ExhaustiveSearch(q=256, n=10, m=10)
+ sage: H.order_of_the_field()
+ 256
+ """
+ return super().order_of_the_field()
\ No newline at end of file
diff --git a/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/f5.py b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/f5.py
new file mode 100644
index 00000000..011b71b4
--- /dev/null
+++ b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/f5.py
@@ -0,0 +1,237 @@
+# *****************************************************************************
+# Multivariate Quadratic (MQ) Estimator
+# Copyright (C) 2021-2022 Emanuele Bellini, Rusydi H. Makarim, Javier Verbel
+# Cryptography Research Centre, Technology Innovation Institute LLC
+#
+# This file is part of MQ Estimator
+#
+# MQ Estimator 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.
+#
+# MQ Estimator 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
+# MQ Estimator. If not, see .
+# *****************************************************************************
+
+
+from sage.functions.other import binomial
+from .base import BaseAlgorithm
+from .. import degree_of_regularity
+
+
+class F5(BaseAlgorithm):
+ """
+ Return an instance of F5 complexity estimator
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``m`` -- no. of polynomials
+ - ``q`` -- order of the base field (default: None)
+ - ``w`` -- linear algebra constant (default: 2)
+ - ``nsolutions`` -- no. of solutions (default: 1)
+ - ``degrees`` -- a list/tuple of degree of the polynomials (default: [2]*m)
+ - ``h`` -- external hybridization parameter (default: 0)
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import F5
+ sage: F5_ = F5(n=10, m=5)
+ sage: F5_
+ Complexity estimator for F5 with 10 variables and 5 polynomials
+ """
+ def __init__(self, n, m, q=None, w=2, nsolutions=1, h=0, **kwargs):
+ if not nsolutions >= 1:
+ raise ValueError("nsolutions must be >= 1")
+
+ degrees = kwargs.get('degrees', [2]*m)
+ if len(degrees) != m:
+ raise ValueError(f"len(degrees) must be equal to {m}")
+
+ super().__init__(n, m, q=q, w=w, h=h)
+ self._nsolutions = nsolutions
+ if degrees == [2]*m:
+ self._degrees = [2]*self.npolynomials_reduced()
+ else:
+ self._degrees = degrees
+ self._time_complexity = None
+ self._memory_complexity = None
+
+ def degree_of_polynomials(self):
+ """
+ Return a list of degree of the polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import F5
+ sage: F5_ = F5(n=10, m=5, degrees=[3]*5)
+ sage: F5_.degree_of_polynomials()
+ [3, 3, 3, 3, 3]
+ """
+ return self._degrees
+
+ def nsolutions(self):
+ """
+ Return the no. of solutions
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import F5
+ sage: F5_ = F5(n=10, m=5, nsolutions=3)
+ sage: F5_.nsolutions()
+ 3
+ """
+ return self._nsolutions
+
+ def time_complexity(self, **kwargs):
+ """
+ Return the time complexity of the F5 algorithm
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import F5
+ sage: F5_ = F5(n=10, m=15, degrees=[3]*15, q=31)
+ sage: F5_.time_complexity()
+ 128005423260
+
+ TESTS::
+
+ sage: F5(n=10, m=15, q=3, degrees=[3]*15).time_complexity()
+ 961920960
+ sage: F5(n=10, m=12, q=5).time_complexity()
+ 769536768
+ sage: F0 = F5(n=15, m=12, q=5)
+ sage: F1 = F5(n=17, m=12, q=5)
+ sage: F0.time_complexity() == F1.time_complexity()
+ True
+ """
+ if self._time_complexity is None:
+ if self.is_overdefined_system():
+ complexity = self.time_complexity_semi_regular_system()
+ else:
+ complexity = self.time_complexity_regular_system()
+
+ if self.nsolutions() == 1:
+ fglm_complexity = 0
+ else:
+ fglm_complexity = self.time_complexity_fglm()
+
+ self._time_complexity = complexity + fglm_complexity
+
+ return self._time_complexity
+
+ def time_complexity_fglm(self):
+ """
+ Return the time complexity of the FGLM algorithm for this system
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import F5
+ sage: F5_ = F5(n=10, m=15, nsolutions=2)
+ sage: F5_.time_complexity_fglm()
+ 80
+ """
+ n = self.nvariables_reduced()
+ D = self.nsolutions()
+ return n * D ** 3
+
+ def time_complexity_regular_system(self):
+ """
+ Return the time complexity for regular system
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import F5
+ sage: F5_ = F5(n=10, m=5, q=31)
+ sage: F5_.time_complexity_regular_system()
+ 63504
+
+ TESTS::
+
+ sage: F5(n=15, m=5, degrees=[2]*5, q=31).time_complexity_regular_system()
+ 3675
+ """
+ if not (self.is_square_system() or self.is_underdefined_system()):
+ raise ValueError("regularity assumption is valid only on square or underdefined system")
+
+ n, m = self.nvariables_reduced(), self.npolynomials_reduced()
+ q = self.order_of_the_field()
+ w = self.linear_algebra_constant()
+ dreg = degree_of_regularity.quadratic_system(n, m, q=q)
+ h = self._h
+ return q ** h * m * binomial(n + dreg, dreg) ** w
+
+ def time_complexity_semi_regular_system(self):
+ """
+ Return the time complexity for semi-regular system
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import F5
+ sage: F5_ = F5(n=5, m=10, q=7)
+ sage: F5_.time_complexity_semi_regular_system()
+ 31360
+
+ TESTS::
+
+ sage: F5(n=5, m=15, degrees=[2]*15, q=7).time_complexity_semi_regular_system()
+ 6615
+ """
+ if not self.is_overdefined_system():
+ raise ValueError("semi regularity assumption is valid only on overdefined system")
+
+ n, m = self.nvariables_reduced(), self.npolynomials_reduced()
+ w = self.linear_algebra_constant()
+ q = self.order_of_the_field()
+ degrees = self.degree_of_polynomials()
+ dreg = degree_of_regularity.semi_regular_system(n, degrees, q)
+ h = self._h
+ return q ** h * m * binomial(n + dreg, dreg) ** w
+
+ def memory_complexity(self):
+ """
+ Return the memory complexity of the F5 algorithm
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import F5
+ sage: F5_ = F5(n=10, m=12, q=5)
+ sage: F5_.memory_complexity()
+ 25050025
+
+ TESTS::
+
+ sage: F0 = F5(n=15, m=12)
+ sage: F1 = F5(n=17, m=12)
+ sage: F0.memory_complexity() == F1.memory_complexity()
+ True
+ """
+ if self._memory_complexity is None:
+ n, m = self.nvariables_reduced(), self.npolynomials_reduced()
+ q = self.order_of_the_field()
+ degrees = self.degree_of_polynomials()
+ dreg = degree_of_regularity.generic_system(n=n, degrees=degrees, q=q)
+ self._memory_complexity = max(binomial(n + dreg - 1, dreg) ** 2, m * n ** 2)
+ return self._memory_complexity
+
+ def tilde_o_time(self):
+ """
+ Return the Ε time complexity of F5 algorithm
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import F5
+ sage: E = F5(n=10, m=12, q=5)
+ sage: E.tilde_o_time()
+ 64128064
+ """
+ return self.time_complexity()/self.npolynomials()
+
+ def __repr__(self):
+ n, m = self.nvariables(), self.npolynomials()
+ return f"Complexity estimator for F5 with {n} variables and {m} polynomials"
diff --git a/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/hybrid_f5.py b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/hybrid_f5.py
new file mode 100644
index 00000000..5bdc6d05
--- /dev/null
+++ b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/hybrid_f5.py
@@ -0,0 +1,430 @@
+# *****************************************************************************
+# Multivariate Quadratic (MQ) Estimator
+# Copyright (C) 2021-2022 Emanuele Bellini, Rusydi H. Makarim, Javier Verbel
+# Cryptography Research Centre, Technology Innovation Institute LLC
+#
+# This file is part of MQ Estimator
+#
+# MQ Estimator 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.
+#
+# MQ Estimator 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
+# MQ Estimator. If not, see .
+# *****************************************************************************
+
+
+from sage.all import Integer
+from .base import BaseAlgorithm, optimal_parameter
+from .f5 import F5
+
+
+class HybridF5(BaseAlgorithm):
+ r"""
+ Construct an instance of HybridF5
+
+ HybridF5 is an algorithm to solve systems of polynomials over a finite field proposed in [BFP09]_, [BFP12]_. The
+ algorithm is a tradeoff between exhaustive search and Groebner bases computation. The idea is to fix the value of,
+ say, $k$ variables and compute the Groebner bases of $q^{k}$ subsystems, where $q$ is the order of the finite
+ field. The Grobner bases computation is done using F5 algorithm.
+
+ .. SEEALSO::
+
+ :class:`mpkc.algorithms.f5.F5` -- class to compute the complexity of F5 algorithm.
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``m`` -- no. of polynomials
+ - ``q`` -- order of the finite field
+ - ``w`` -- linear algebra constant (2 <= w <= 3) (default: 2)
+ - ``use_quantum`` -- return the complexity using quantum computer (default: False)
+ - ``degrees`` -- a list/tuple of degree of the polynomials (default: [2]*m, i.e. quadratic system)
+ - ``h`` -- external hybridization parameter (default: 0)
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import HybridF5
+ sage: H = HybridF5(q=256, n=5, m=10)
+ sage: H
+ Complexity estimator for hybrid approach with 5 variables and 10 polynomials
+ """
+ def __init__(self, n, m, q, w=2, use_quantum=False, h=0, **kwargs):
+ if not isinstance(q, (int, Integer)):
+ raise TypeError("q must be an integer")
+
+ degrees = kwargs.get('degrees', [2] * m)
+
+ if len(degrees) != m:
+ raise ValueError(f"len(degrees) must be equal to {m}")
+
+ super().__init__(n, m, q=q, w=w, h=h)
+ self._use_quantum = use_quantum
+ if degrees == [2] * m:
+ self._degrees = [2] * self.npolynomials_reduced()
+ else:
+ self._degrees = degrees
+ self._time_complexity = None
+ self._memory_complexity = None
+
+ def degree_of_polynomials(self):
+ """
+ Return a list of degree of the polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import HybridF5
+ sage: H = HybridF5(q=31, n=5, m=5, degrees=[3]*5)
+ sage: H.degree_of_polynomials()
+ [3, 3, 3, 3, 3]
+ """
+ return self._degrees
+
+ def use_quantum(self):
+ """
+ Return `True` if the complexity computation is in quantum model
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import HybridF5
+ sage: H = HybridF5(q=31, n=5, m=10, use_quantum=False)
+ sage: H.use_quantum()
+ False
+ sage: H = HybridF5(q=31, n=5, m=10, use_quantum=True)
+ sage: H.use_quantum()
+ True
+ """
+ return self._use_quantum
+
+ @optimal_parameter
+ def k(self):
+ """
+ Return `k`, i.e. the optimal no. of fixed variables
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import HybridF5
+ sage: H = HybridF5(q=31, n=23, m=23)
+ sage: H.k()
+ 2
+
+ TESTS::
+
+ sage: H = HybridF5(q=256, n=10, m=10)
+ sage: H.k()
+ 1
+ sage: H = HybridF5(q=256, n=20, m=10)
+ sage: H.k()
+ 1
+ """
+ n = self.nvariables_reduced()
+ complexities = [self._time_complexity_(k) for k in range(n)]
+ return min(range(len(complexities)), key=complexities.__getitem__)
+
+ def time_complexity(self, **kwargs):
+ """
+ Return the complexity of HybridF5
+
+ INPUT:
+
+ - ``k`` -- no. of fixed variables (default: None)
+
+ .. NOTE::
+
+ If ``k`` is specified, the function returns the time complexity w.r.t the given parameter
+
+ .. SEEALSO::
+
+ :meth:`mpkc.algorithms.f5.F5.time_complexity`
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import HybridF5
+ sage: H = HybridF5(q=256, n=10, m=10)
+ sage: H.time_complexity()
+ 64128064000
+ sage: H.time_complexity(k=2)
+ 1085517987840
+
+ TESTS::
+
+ sage: H = HybridF5(q=256, n=10, m=15)
+ sage: H.time_complexity()
+ 15030015
+ sage: H.time_complexity(k=2)
+ 26763264000
+ """
+ k = kwargs.get('k', self.k())
+
+ if k == self.k():
+ if self._time_complexity is None:
+ self._time_complexity = self._time_complexity_(k)
+ time_complexity = self._time_complexity
+ else:
+ time_complexity = self._time_complexity
+ else:
+ n = self.nvariables_reduced()
+ if not 0 <= k <= n:
+ raise ValueError(f'k must be in the range 0 <= k <= {n}')
+ else:
+ time_complexity = self._time_complexity_(k)
+
+ h = self._h
+ q = self.order_of_the_field()
+ time_complexity *= q ** h
+ return time_complexity
+
+ def memory_complexity(self, **kwargs):
+ """
+ Return the memory complexity of HybridF5
+
+ INPUT:
+
+ - ``k`` -- no. of fixed variables (default: None)
+
+ .. NOTE::
+
+ If ``k`` is specified, the function returns the time complexity w.r.t the given parameter
+
+
+ .. SEEALSO::
+
+ :meth:`mpkc.algorithms.f5.F5.memory_complexity`
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import HybridF5
+ sage: E = HybridF5(n=10, m=12, q=7)
+ sage: E.memory_complexity()
+ 7056
+ sage: E.memory_complexity(k=1)
+ 1656369
+ """
+ k = kwargs.get('k', self.k())
+
+ n, m = self.nvariables_reduced(), self.npolynomials_reduced()
+ q = self.order_of_the_field()
+ w = self.linear_algebra_constant()
+ degrees = self.degree_of_polynomials()
+
+ if k == self.k():
+ if self._memory_complexity is None:
+ memory_complexity = F5(n=n-k, m=m, q=q, w=w, degrees=degrees).memory_complexity()
+ self._memory_complexity = memory_complexity
+ else:
+ memory_complexity = self._memory_complexity
+ else:
+ memory_complexity = F5(n=n - k, m=m, q=q, w=w, degrees=degrees).memory_complexity()
+
+ return memory_complexity
+
+ def tilde_o_time(self):
+ """
+ Return the Ε time complexity of HybridF5
+
+ .. SEEALSO::
+
+ :meth:`mpkc.algorithms.f5.F5.tilde_o_time`
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import HybridF5
+ sage: E = HybridF5(n=10, m=12, q=7)
+ sage: E.tilde_o_time()
+ 59270400
+ """
+ return self.time_complexity()
+
+ def _time_complexity_(self, k):
+ """
+ Return the time complexity w.r.t. `k`.
+
+ INPUT:
+
+ - ``k`` -- no. of fixed variables
+ """
+ n, m = self.nvariables_reduced(), self.npolynomials_reduced()
+ q = self.order_of_the_field()
+ w = self.linear_algebra_constant()
+ degrees = self.degree_of_polynomials()
+
+ return q ** (k / 2 if self.use_quantum() else k) * F5(n=n-k, m=m, q=q, w=w, degrees=degrees).time_complexity()
+
+ def __repr__(self):
+ n, m = self.nvariables(), self.npolynomials_reduced()
+ return f"Complexity estimator for hybrid approach with {n} variables and {m} polynomials"
+
+ # all methods below are implemented to overwrite the parent's docstring while keeping the implementation
+
+ def has_optimal_parameter(self):
+ """
+ Return `True` if the algorithm has optimal parameter
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import HybridF5
+ sage: H = HybridF5(q=256, n=5, m=10)
+ sage: H.has_optimal_parameter()
+ True
+ """
+ return super().has_optimal_parameter()
+
+ def is_defined_over_finite_field(self):
+ """
+ Return `True` if the algorithm is defined over a finite field
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import HybridF5
+ sage: H = HybridF5(q=256, n=5, m=10)
+ sage: H.is_defined_over_finite_field()
+ True
+ """
+ return super().is_defined_over_finite_field()
+
+ def is_overdefined_system(self):
+ """
+ Return `True` if the system is overdefined
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import HybridF5
+ sage: H = HybridF5(q=256, n=5, m=10)
+ sage: H.is_overdefined_system()
+ True
+ sage: E = HybridF5(q=256, n=10, m=10)
+ sage: E.is_overdefined_system()
+ False
+ """
+ return super().is_overdefined_system()
+
+ def is_square_system(self):
+ """
+ Return `True` if the system is square, there are equal no. of variables and polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import HybridF5
+ sage: H = HybridF5(q=256, n=5, m=10)
+ sage: H.is_square_system()
+ False
+ sage: E = HybridF5(q=256, n=10, m=10)
+ sage: E.is_square_system()
+ True
+ """
+ return super().is_square_system()
+
+ def is_underdefined_system(self):
+ """
+ Return `True` if the system is underdefined
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import HybridF5
+ sage: H = HybridF5(q=256, n=5, m=10)
+ sage: H.is_underdefined_system()
+ False
+ sage: E = HybridF5(q=256, n=10, m=5)
+ sage: E.is_underdefined_system()
+ True
+ """
+ return super().is_underdefined_system()
+
+ def linear_algebra_constant(self):
+ """
+ Return the linear algebra constant
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import HybridF5
+ sage: H = HybridF5(q=256, n=5, m=10, w=2)
+ sage: H.linear_algebra_constant()
+ 2
+ """
+ return super().linear_algebra_constant()
+
+ def npolynomials(self):
+ """
+ Return the number of polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import HybridF5
+ sage: H = HybridF5(q=256, n=5, m=10)
+ sage: H.npolynomials()
+ 10
+ """
+ return super().npolynomials()
+
+ def nvariables(self):
+ """
+ Return the number of variables
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import HybridF5
+ sage: H = HybridF5(q=256, n=5, m=10)
+ sage: H.nvariables()
+ 5
+ """
+ return super().nvariables()
+
+ def nvariables_reduced(self):
+ """
+ Return the no. of variables after fixing some values
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import HybridF5
+ sage: H = HybridF5(q=256, n=5, m=10)
+ sage: H.nvariables_reduced()
+ 5
+ sage: E = HybridF5(q=256, n=12, m=10)
+ sage: E.nvariables_reduced()
+ 10
+ """
+ return super().nvariables_reduced()
+
+ def npolynomials_reduced(self):
+ """
+ Return the no. of polynomials after applying the Thomae and Wolf strategy
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import HybridF5
+ sage: H = HybridF5(q=256, n=5, m=10)
+ sage: H.npolynomials_reduced()
+ 10
+ """
+ return super().npolynomials_reduced()
+
+ def optimal_parameters(self):
+ """
+ Return a dictionary of optimal parameters
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import HybridF5
+ sage: H = HybridF5(q=256, n=15, m=10)
+ sage: H.optimal_parameters()
+ {'k': 1}
+ """
+ return super().optimal_parameters()
+
+ def order_of_the_field(self):
+ """
+ Return the order of the field
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import HybridF5
+ sage: H = HybridF5(q=256, n=15, m=10)
+ sage: H.order_of_the_field()
+ 256
+ """
+ return super().order_of_the_field()
diff --git a/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/kpg.py b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/kpg.py
new file mode 100644
index 00000000..503465c3
--- /dev/null
+++ b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/kpg.py
@@ -0,0 +1,267 @@
+# *****************************************************************************
+# Multivariate Quadratic (MQ) Estimator
+# Copyright (C) 2021-2022 Emanuele Bellini, Rusydi H. Makarim, Javier Verbel
+# Cryptography Research Centre, Technology Innovation Institute LLC
+#
+# This file is part of MQ Estimator
+#
+# MQ Estimator 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.
+#
+# MQ Estimator 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
+# MQ Estimator. If not, see .
+# *****************************************************************************
+
+
+from sage.all import Integer
+from sage.arith.misc import is_power_of_two
+from .base import BaseAlgorithm
+
+
+class KPG(BaseAlgorithm):
+ r"""
+ Construct an instance of KPG estimator
+
+ The KPG is an algorithm to solve a quadratic systems of equations over fields of even characteristic [KPG99]_.
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``m`` -- no. of polynomials
+ - ``q`` -- order of the finite field
+ - ``w`` -- linear algebra constant (2 <= w <= 3) (default: 2)
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import KPG
+ sage: E = KPG(n=183, m=12, q=4, w=2.8)
+ sage: E
+ KPG estimator for the MQ problem
+
+ TESTS::
+
+ sage: E.nvariables() == E.nvariables_reduced()
+ True
+ """
+ def __init__(self, n, m, q, w=2):
+ if not isinstance(q, (int, Integer)):
+ raise TypeError("q must be an integer")
+
+ if not is_power_of_two(q):
+ raise ValueError("the order of finite field q must be a power of 2")
+
+ if not m * (m + 1) < n:
+ raise ValueError(f'The condition m(m + 1) < n must be satisfied')
+
+ super().__init__(n=n, m=m, q=q, w=w)
+ self._n_reduced = n
+ self._m_reduced = m
+
+ def time_complexity(self):
+ """
+ Return the time complexity of kpg algorithm
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import KPG
+ sage: E = KPG(n=183, m=12, q=4, w=2.8)
+ sage: float(log(E.time_complexity(),2))
+ 24.628922047916475
+ """
+ n, m = self.nvariables(), self.npolynomials()
+ w = self.linear_algebra_constant()
+ return m * n ** w
+
+ def memory_complexity(self):
+ """
+ Return the memory complexity of kpg algorithm
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import KPG
+ sage: E = KPG(n=183, m=12, q=4, w=2.8)
+ sage: E.memory_complexity()
+ 401868
+ """
+ n, m = self.nvariables(), self.npolynomials()
+ return m * n ** 2
+
+ def tilde_o_time(self):
+ """
+ Return the Ε time complexity
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import KPG
+ sage: E = KPG(n=183, m=12, q=4, w=2.8)
+ sage: E.tilde_o_time()
+ 1
+ """
+ return 1
+
+ def __repr__(self):
+ return f"KPG estimator for the MQ problem"
+
+ # all methods below are implemented to overwrite the parent's docstring while keeping the implementation
+
+ def has_optimal_parameter(self):
+ """
+ Return `True` if the algorithm has optimal parameter
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import KPG
+ sage: H = KPG(n=183, m=12, q=4, w=2.8)
+ sage: H.has_optimal_parameter()
+ False
+ """
+ return super().has_optimal_parameter()
+
+ def is_defined_over_finite_field(self):
+ """
+ Return `True` if the algorithm is defined over a finite field
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import KPG
+ sage: H = KPG(n=183, m=12, q=4, w=2.8)
+ sage: H.is_defined_over_finite_field()
+ True
+ """
+ return super().is_defined_over_finite_field()
+
+ def is_overdefined_system(self):
+ """
+ Return `True` if the system is overdefined
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import KPG
+ sage: H = KPG(n=183, m=12, q=4, w=2.8)
+ sage: H.is_overdefined_system()
+ False
+ """
+ return super().is_overdefined_system()
+
+ def is_square_system(self):
+ """
+ Return `True` if the system is square, there are equal no. of variables and polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import KPG
+ sage: H = KPG(n=183, m=12, q=4, w=2.8)
+ sage: H.is_square_system()
+ False
+ """
+ return super().is_square_system()
+
+ def is_underdefined_system(self):
+ """
+ Return `True` if the system is underdefined
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import KPG
+ sage: E = KPG(n=183, m=12, q=4, w=2.8)
+ sage: E.is_underdefined_system()
+ True
+ """
+ return super().is_underdefined_system()
+
+ def linear_algebra_constant(self):
+ """
+ Return the linear algebra constant
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import KPG
+ sage: H = KPG(n=183, m=12, q=4, w=3)
+ sage: H.linear_algebra_constant()
+ 3
+ """
+ return super().linear_algebra_constant()
+
+ def npolynomials(self):
+ """
+ Return the number of polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import KPG
+ sage: H = KPG(n=183, m=12, q=4, w=2.8)
+ sage: H.npolynomials()
+ 12
+ """
+ return super().npolynomials()
+
+ def nvariables(self):
+ """
+ Return the number of variables
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import KPG
+ sage: H = KPG(n=183, m=12, q=4, w=2.8)
+ sage: H.nvariables()
+ 183
+ """
+ return super().nvariables()
+
+ def nvariables_reduced(self):
+ """
+ Return the no. of variables after fixing some values
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import KPG
+ sage: H = KPG(n=183, m=12, q=4, w=2.8)
+ sage: H.nvariables_reduced()
+ 183
+ """
+ return super().nvariables_reduced()
+
+ def npolynomials_reduced(self):
+ """
+ Return the no. of polynomials after applying the Thomae and Wolf strategy
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import KPG
+ sage: H = H = KPG(n=183, m=12, q=4, w=2.8)
+ sage: H.npolynomials_reduced()
+ 12
+ """
+ return super().npolynomials_reduced()
+
+ def optimal_parameters(self):
+ """
+ Return a dictionary of optimal parameters
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import KPG
+ sage: H = KPG(n=183, m=12, q=4, w=2.8)
+ sage: H.optimal_parameters()
+ {}
+ """
+ return super().optimal_parameters()
+
+ def order_of_the_field(self):
+ """
+ Return the order of the field
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import KPG
+ sage: H = KPG(n=183, m=12, q=4, w=2.8)
+ sage: H.order_of_the_field()
+ 4
+ """
+ return super().order_of_the_field()
\ No newline at end of file
diff --git a/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/lokshtanov.py b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/lokshtanov.py
new file mode 100644
index 00000000..9325fa10
--- /dev/null
+++ b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/lokshtanov.py
@@ -0,0 +1,376 @@
+# *****************************************************************************
+# Multivariate Quadratic (MQ) Estimator
+# Copyright (C) 2021-2022 Emanuele Bellini, Rusydi H. Makarim, Javier Verbel
+# Cryptography Research Centre, Technology Innovation Institute LLC
+#
+# This file is part of MQ Estimator
+#
+# MQ Estimator 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.
+#
+# MQ Estimator 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
+# MQ Estimator. If not, see .
+# *****************************************************************************
+
+
+from sage.all import Integer
+from sage.arith.misc import is_power_of_two
+from sage.functions.log import log
+from sage.functions.other import floor
+from sage.rings.infinity import Infinity
+from sage.rings.finite_rings.finite_field_constructor import GF
+from .base import BaseAlgorithm, optimal_parameter
+from ..series.nmonomial import NMonomialSeries
+
+
+class Lokshtanov(BaseAlgorithm):
+ r"""
+ Construct an instance of Lokshtanov et al.'s estimator
+
+ Lokshtanov et al.'s is a probabilistic algorithm to solve the MQ problem over GF(q) [LPTWY17]_. It describes an
+ algorithm to determine the consistency of a given system of polynomial equations.
+
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``m`` -- no. of polynomials
+ - ``q`` -- order of the finite field
+ - ``h`` -- external hybridization parameter (default: 0)
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Lokshtanov
+ sage: E = Lokshtanov(n=10, m=12, q=9)
+ sage: E
+ Lokshtanov et al.'s estimator for the MQ problem
+ """
+ def __init__(self, n, m, q, h=0):
+ if not isinstance(q, (int, Integer)):
+ raise TypeError("q must be an integer")
+
+ if q > 1024:
+ raise TypeError("q too big to run this algorithm")
+
+ super().__init__(n=n, m=m, q=q, h=h)
+ self._time_complexity = None
+ self._memory_complexity = None
+
+ @optimal_parameter
+ def Ξ΄(self):
+ r"""
+ Return the optimal `\delta` for Lokshtanov et al.'s algorithm
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Lokshtanov
+ sage: E = Lokshtanov(n=10, m=12, q=9)
+ sage: E.Ξ΄()
+ 1/10
+ """
+ min_complexity = Infinity
+ optimal_Ξ΄ = None
+ n, m = self.nvariables_reduced(), self.npolynomials_reduced()
+
+ for np in range(1, min(m - 2, n)):
+ Ξ΄ = np / n
+ time_complexity = self._C(n - 1, Ξ΄)
+ if time_complexity < min_complexity:
+ min_complexity = time_complexity
+ optimal_Ξ΄ = Ξ΄
+
+ return optimal_Ξ΄
+
+ def time_complexity(self, **kwargs):
+ r"""
+ Return the time complexity of lokshtanov et al.'s algorithm
+
+ INPUT:
+
+ - ``Ξ΄`` -- the parameter `\delta`
+
+ If `\delta` is specified, the function returns the time complexity w.r.t. the given parameter
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Lokshtanov
+ sage: E = Lokshtanov(n=10, m=12, q=9)
+ sage: float(log(E.time_complexity(), 2))
+ 212.576588724275
+ sage: float(log(E.time_complexity(Ξ΄=2/10), 2))
+ 214.16804105519708
+
+ TESTS::
+
+ sage: E0 = Lokshtanov(n=15, m=12, q=9)
+ sage: E1 = Lokshtanov(n=17, m=12, q=9)
+ sage: E0.time_complexity().numerical_approx() == E1.time_complexity().numerical_approx()
+ True
+ """
+ q = self.order_of_the_field()
+ n = self.nvariables_reduced()
+ Ξ΄ = kwargs.get('Ξ΄', self.Ξ΄())
+
+ if Ξ΄ is None:
+ return Infinity
+ else:
+ if not 0 < Ξ΄ < 1:
+ raise ValueError("Ξ΄ must be in the range 0 < Ξ΄ < 1")
+
+ if Ξ΄ == self.Ξ΄():
+ if self._time_complexity is None:
+ self._time_complexity = 100 * log(q, 2) * (q - 1) * sum([self._C(n - i, Ξ΄) for i in range(1, n)])
+ time_complexity = self._time_complexity
+ else:
+ time_complexity = self._time_complexity
+ else:
+ time_complexity = 100 * log(q, 2) * (q - 1) * sum([self._C(n - i, Ξ΄) for i in range(1, n)])
+
+ h = self._h
+ time_complexity *= q ** h
+ return time_complexity
+
+ def memory_complexity(self):
+ """
+ Return the memory complexity of Lokshtanov et al.'s algorithm
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Lokshtanov
+ sage: E = Lokshtanov(n=10, m=12, q=9)
+ sage: float(log(E.memory_complexity(), 2))
+ 30.622995719758727
+
+ TESTS::
+
+ sage: E0 = Lokshtanov(n=15, m=12, q=9)
+ sage: E1 = Lokshtanov(n=17, m=12, q=9)
+ sage: E0.memory_complexity().numerical_approx() == E1.memory_complexity().numerical_approx()
+ True
+ """
+ if self._memory_complexity is None:
+ Ξ΄ = self.Ξ΄()
+ if Ξ΄ is None:
+ return Infinity
+
+ n = self.nvariables_reduced()
+ q = self.order_of_the_field()
+
+ np = floor(n * Ξ΄)
+ resulting_degree = 2 * (q - 1) * (np + 2)
+ M = NMonomialSeries(n=n - np, q=q, max_prec=resulting_degree + 1).nmonomials_up_to_degree(resulting_degree)
+ self._memory_complexity = M + log(n, 2) * q ** (n - np)
+ return self._memory_complexity
+
+ def tilde_o_time(self):
+ r"""
+ Return the `\widetilde{O}` time complexity of Lokshtanov et al.'s algorithm
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Lokshtanov
+ sage: E = Lokshtanov(n=10, m=12, q=9)
+ sage: float(log(E.tilde_o_time(), 2))
+ 31.62000188938707
+ """
+ e = 2.718
+ q = self.order_of_the_field()
+ n = self.nvariables_reduced()
+ if q == 2:
+ time = q ** (0.8765 * n)
+ elif is_power_of_two(q):
+ time = q ** (0.9 * n)
+ elif log(GF(q).characteristic(), 2) < 8 * e:
+ time = q ** (0.9975 * n)
+ else:
+ d = GF(q).degree()
+ time = q ** n * (log(q, 2) / (2 * e * d)) ** (-d * n)
+
+ h = self._h
+ return q ** h * time
+
+ def _C(self, n, delta):
+ q = self.order_of_the_field()
+ np = floor(delta * n)
+ resulting_degree = 2 * (q - 1) * (np + 2)
+ M = NMonomialSeries(n=n - np, q=q, max_prec=resulting_degree + 1).nmonomials_up_to_degree(resulting_degree)
+ return n * (q ** (n - np) + M * q ** np * n ** (6 * q))
+
+ def __repr__(self):
+ return f"Lokshtanov et al.'s estimator for the MQ problem"
+
+ # all methods below are implemented to overwrite the parent's docstring while keeping the implementation
+
+ def has_optimal_parameter(self):
+ """
+ Return `True` if the algorithm has optimal parameter
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Lokshtanov
+ sage: H = Lokshtanov(q=256, n=5, m=10)
+ sage: H.has_optimal_parameter()
+ True
+ """
+ return super().has_optimal_parameter()
+
+ def is_defined_over_finite_field(self):
+ """
+ Return `True` if the algorithm is defined over a finite field
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Lokshtanov
+ sage: H = Lokshtanov(q=256, n=5, m=10)
+ sage: H.is_defined_over_finite_field()
+ True
+ """
+ return super().is_defined_over_finite_field()
+
+ def is_overdefined_system(self):
+ """
+ Return `True` if the system is overdefined
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Lokshtanov
+ sage: H = Lokshtanov(q=256, n=5, m=10)
+ sage: H.is_overdefined_system()
+ True
+ sage: E = Lokshtanov(q=256, n=10, m=10)
+ sage: E.is_overdefined_system()
+ False
+ """
+ return super().is_overdefined_system()
+
+ def is_square_system(self):
+ """
+ Return `True` if the system is square, there are equal no. of variables and polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Lokshtanov
+ sage: H = Lokshtanov(q=256, n=5, m=10)
+ sage: H.is_square_system()
+ False
+ sage: E = Lokshtanov(q=256, n=10, m=10)
+ sage: E.is_square_system()
+ True
+ """
+ return super().is_square_system()
+
+ def is_underdefined_system(self):
+ """
+ Return `True` if the system is underdefined
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Lokshtanov
+ sage: H = Lokshtanov(q=256, n=5, m=10)
+ sage: H.is_underdefined_system()
+ False
+ sage: E = Lokshtanov(q=256, n=10, m=5)
+ sage: E.is_underdefined_system()
+ True
+ """
+ return super().is_underdefined_system()
+
+ def linear_algebra_constant(self):
+ """
+ Return the linear algebra constant
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Lokshtanov
+ sage: H = Lokshtanov(q=256, n=5, m=10)
+ sage: H.linear_algebra_constant()
+
+ """
+ return super().linear_algebra_constant()
+
+ def npolynomials(self):
+ """
+ Return the number of polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Lokshtanov
+ sage: H = Lokshtanov(q=256, n=5, m=10)
+ sage: H.npolynomials()
+ 10
+ """
+ return super().npolynomials()
+
+ def nvariables(self):
+ """
+ Return the number of variables
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Lokshtanov
+ sage: H = Lokshtanov(q=256, n=5, m=10)
+ sage: H.nvariables()
+ 5
+ """
+ return super().nvariables()
+
+ def nvariables_reduced(self):
+ """
+ Return the no. of variables after fixing some values
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Lokshtanov
+ sage: H = Lokshtanov(q=256, n=5, m=10)
+ sage: H.nvariables_reduced()
+ 5
+ sage: E = Lokshtanov(q=256, n=12, m=10)
+ sage: E.nvariables_reduced()
+ 10
+ """
+ return super().nvariables_reduced()
+
+ def npolynomials_reduced(self):
+ """
+ Return the no. of polynomials after applying the Thomae and Wolf strategy
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Lokshtanov
+ sage: H = Lokshtanov(q=256, n=5, m=10)
+ sage: H.npolynomials_reduced()
+ 10
+ """
+ return super().npolynomials_reduced()
+
+ def optimal_parameters(self):
+ """
+ Return a dictionary of optimal parameters
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Lokshtanov
+ sage: H = Lokshtanov(q=256, n=15, m=10)
+ sage: H.optimal_parameters()
+ {'Ξ΄': 1/10}
+ """
+ return super().optimal_parameters()
+
+ def order_of_the_field(self):
+ """
+ Return the order of the field
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import Lokshtanov
+ sage: H = Lokshtanov(q=256, n=15, m=10)
+ sage: H.order_of_the_field()
+ 256
+ """
+ return super().order_of_the_field()
\ No newline at end of file
diff --git a/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/mht.py b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/mht.py
new file mode 100644
index 00000000..fa03cbb5
--- /dev/null
+++ b/tests/module/multivariate_quadratic_estimator/mpkc/algorithms/mht.py
@@ -0,0 +1,270 @@
+# *****************************************************************************
+# Multivariate Quadratic (MQ) Estimator
+# Copyright (C) 2021-2022 Emanuele Bellini, Rusydi H. Makarim, Javier Verbel
+# Cryptography Research Centre, Technology Innovation Institute LLC
+#
+# This file is part of MQ Estimator
+#
+# MQ Estimator 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.
+#
+# MQ Estimator 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
+# MQ Estimator. If not, see .
+# *****************************************************************************
+
+
+from sage.all import Integer
+from sage.arith.misc import is_power_of_two
+from .base import BaseAlgorithm
+
+
+class MHT(BaseAlgorithm):
+ r"""
+ Construct an instance of MHT estimator
+
+ The MHT is an algorithm to solve the MQ problem when $m (m + 3) / 2 \leq n$ [MHT13]_.
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``m`` -- no. of polynomials
+ - ``q`` -- order of the finite field
+ - ``w`` -- linear algebra constant (2 <= w <= 3) (default: 2)
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import MHT
+ sage: E = MHT(n=183, m=12, q=4, w=2.8)
+ sage: E
+ MHT estimator for the MQ problem
+
+ TESTS::
+
+ sage: E.nvariables() == E.nvariables_reduced()
+ True
+ """
+ def __init__(self, n, m, q, w=2):
+ if not isinstance(q, (int, Integer)):
+ raise TypeError("q must be an integer")
+
+ if not m * (m + 3) / 2 <= n:
+ raise ValueError(f'The parameter n should be grater than or equal to m * (m + 3) / 2')
+
+ super().__init__(n=n, m=m, q=q, w=w)
+ self._n_reduced = n
+ self._m_reduced = m
+
+ def time_complexity(self):
+ """
+ Return the time complexity of MHT algorithm
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import MHT
+ sage: E = MHT(n=183, m=12, q=4, w=2.8)
+ sage: float(log(E.time_complexity(),2))
+ 24.628922047916475
+ """
+ n, m = self.nvariables(), self.npolynomials()
+ w = self.linear_algebra_constant()
+
+ if is_power_of_two(self.order_of_the_field()):
+ time = n ** w * m
+ else:
+ time = 2 ** m * n ** w * m
+
+ return time
+
+ def memory_complexity(self):
+ """
+ Return the memory complexity of MHT algorithm
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import MHT
+ sage: E = MHT(n=183, m=12, q=4, w=2.8)
+ sage: E.memory_complexity()
+ 401868
+ """
+ n, m = self.nvariables(), self.npolynomials()
+ return m * n ** 2
+
+ def tilde_o_time(self):
+ """
+ Return the Ε time complexity
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import MHT
+ sage: E = MHT(n=183, m=12, q=4, w=2.8)
+ sage: E.tilde_o_time()
+ 1
+ """
+ return 1
+
+ def __repr__(self):
+ return f"MHT estimator for the MQ problem"
+
+ # all methods below are implemented to overwrite the parent's docstring while keeping the implementation
+
+ def has_optimal_parameter(self):
+ """
+ Return `True` if the algorithm has optimal parameter
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import MHT
+ sage: H = MHT(n=183, m=12, q=4, w=2.8)
+ sage: H.has_optimal_parameter()
+ False
+ """
+ return super().has_optimal_parameter()
+
+ def is_defined_over_finite_field(self):
+ """
+ Return `True` if the algorithm is defined over a finite field
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import MHT
+ sage: H = MHT(n=183, m=12, q=4, w=2.8)
+ sage: H.is_defined_over_finite_field()
+ True
+ """
+ return super().is_defined_over_finite_field()
+
+ def is_overdefined_system(self):
+ """
+ Return `True` if the system is overdefined
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import MHT
+ sage: H = MHT(n=183, m=12, q=4, w=2.8)
+ sage: H.is_overdefined_system()
+ False
+ """
+ return super().is_overdefined_system()
+
+ def is_square_system(self):
+ """
+ Return `True` if the system is square, there are equal no. of variables and polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import MHT
+ sage: H = MHT(n=183, m=12, q=4, w=2.8)
+ sage: H.is_square_system()
+ False
+ """
+ return super().is_square_system()
+
+ def is_underdefined_system(self):
+ """
+ Return `True` if the system is underdefined
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import MHT
+ sage: E = MHT(n=183, m=12, q=4, w=2.8)
+ sage: E.is_underdefined_system()
+ True
+ """
+ return super().is_underdefined_system()
+
+ def linear_algebra_constant(self):
+ """
+ Return the linear algebra constant
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import MHT
+ sage: H = MHT(n=183, m=12, q=4, w=3)
+ sage: H.linear_algebra_constant()
+ 3
+ """
+ return super().linear_algebra_constant()
+
+ def npolynomials(self):
+ """
+ Return the number of polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import MHT
+ sage: H = MHT(n=183, m=12, q=4, w=2.8)
+ sage: H.npolynomials()
+ 12
+ """
+ return super().npolynomials()
+
+ def nvariables(self):
+ """
+ Return the number of variables
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import MHT
+ sage: H = MHT(n=183, m=12, q=4, w=2.8)
+ sage: H.nvariables()
+ 183
+ """
+ return super().nvariables()
+
+ def nvariables_reduced(self):
+ """
+ Return the no. of variables after fixing some values
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import MHT
+ sage: H = MHT(n=183, m=12, q=4, w=2.8)
+ sage: H.nvariables_reduced()
+ 183
+ """
+ return super().nvariables_reduced()
+
+ def npolynomials_reduced(self):
+ """
+ Return the no. of polynomials after applying the Thomae and Wolf strategy
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import MHT
+ sage: H = MHT(n=183, m=12, q=4, w=2.8)
+ sage: H.npolynomials_reduced()
+ 12
+ """
+ return super().npolynomials_reduced()
+
+ def optimal_parameters(self):
+ """
+ Return a dictionary of optimal parameters
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import MHT
+ sage: H = MHT(n=183, m=12, q=4, w=2.8)
+ sage: H.optimal_parameters()
+ {}
+ """
+ return super().optimal_parameters()
+
+ def order_of_the_field(self):
+ """
+ Return the order of the field
+
+ EXAMPLES::
+
+ sage: from mpkc.algorithms import MHT
+ sage: H = MHT(n=183, m=12, q=4, w=2.8)
+ sage: H.order_of_the_field()
+ 4
+ """
+ return super().order_of_the_field()
\ No newline at end of file
diff --git a/tests/module/multivariate_quadratic_estimator/mpkc/degree_of_regularity.py b/tests/module/multivariate_quadratic_estimator/mpkc/degree_of_regularity.py
new file mode 100644
index 00000000..6613be84
--- /dev/null
+++ b/tests/module/multivariate_quadratic_estimator/mpkc/degree_of_regularity.py
@@ -0,0 +1,157 @@
+# *****************************************************************************
+# Multivariate Quadratic (MQ) Estimator
+# Copyright (C) 2021-2022 Emanuele Bellini, Rusydi H. Makarim, Javier Verbel
+# Cryptography Research Centre, Technology Innovation Institute LLC
+#
+# This file is part of MQ Estimator
+#
+# MQ Estimator 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.
+#
+# MQ Estimator 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
+# MQ Estimator. If not, see .
+# *****************************************************************************
+
+
+from .series.hilbert import HilbertSeries
+
+
+def generic_system(n, degrees, q=None):
+ """
+ Return the degree of regularity for the system of polynomial equations
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``degrees`` -- a list of integers representing the degree of the polynomials
+ - ``q`` -- order of the finite field (default: None)
+
+ EXAMPLES::
+
+ sage: from mpkc import degree_of_regularity
+ sage: degree_of_regularity.generic_system(5, [2]*10)
+ 3
+
+ TESTS::
+
+ sage: degree_of_regularity.generic_system(10, [3]*5)
+ Traceback (most recent call last):
+ ...
+ ValueError: degree of regularity is defined for system with n <= m
+ """
+ m = len(degrees)
+
+ if not n <= m:
+ raise ValueError("degree of regularity is defined for system with n <= m")
+
+ return semi_regular_system(n=n, degrees=degrees, q=q)
+
+
+def regular_system(n, degrees):
+ """
+ Return the degree of regularity for regular system
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``degree`` -- a list of integers representing the degree of the polynomials
+
+ .. NOTE::
+
+ The degree of regularity for regular system is defined only for systems with equal numbers of variables
+ and polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc import degree_of_regularity
+ sage: degree_of_regularity.regular_system(15, [2]*15)
+ 16
+
+ TESTS::
+
+ sage: from mpkc import degree_of_regularity
+ sage: degree_of_regularity.regular_system(15, [2]*16)
+ Traceback (most recent call last):
+ ...
+ ValueError: the number of variables must be equal to the number of polynomials
+ """
+ m = len(degrees)
+ if not n == m:
+ raise ValueError("the number of variables must be equal to the number of polynomials")
+ return semi_regular_system(n=n, degrees=degrees)
+
+
+def semi_regular_system(n, degrees, q=None):
+ r"""
+ Return the degree of regularity for semi-regular system
+
+ The degree of regularity of a semi-regular system $(f_1, \ldots, f_m)$ of respective degrees $d_1, \ldots, d_m$ is
+ given by the index of the first non-positive coefficient of
+
+ .. MATH::
+
+ \dfrac{\prod_{i=1}^{m} (1 - z^{d_i})}{(1 - z)^{n}}
+
+ If the system is defined over a finite field of order `q`, then it is the index of the first non-positive
+ coefficient of the following sequence
+
+ .. MATH::
+
+ \prod_{i=1}^{m} \dfrac{(1 - z^{d_i})}{(1 - z^{q d_i})} \cdot \Bigg( \dfrac{(1 - z^{q})}{(1 - z)} \Bigg)^{n}
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``degrees`` -- a list of integers representing the degree of the polynomials
+ - ``q`` -- order of the finite field (default: None)
+
+ EXAMPLES::
+
+ sage: from mpkc import degree_of_regularity
+ sage: degree_of_regularity.semi_regular_system(10, [2]*15)
+ 4
+ sage: degree_of_regularity.semi_regular_system(10, [2]*15, q=2)
+ 3
+
+ TESTS::
+
+ sage: degree_of_regularity.semi_regular_system(10, [2]*9)
+ Traceback (most recent call last):
+ ...
+ ValueError: the number of polynomials must be >= than the number of variables
+ """
+ m = len(degrees)
+ if not m >= n:
+ raise ValueError("the number of polynomials must be >= than the number of variables")
+
+ s = HilbertSeries(n, degrees, q=q)
+ return s.first_nonpositive_integer()
+
+
+def quadratic_system(n, m, q=None):
+ """
+ Return the degree of regularity for quadratic system
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``m`` -- no. of polynomials
+ - ``q`` -- order of the finite field (default: None)
+
+ EXAMPLES::
+
+ sage: from mpkc import degree_of_regularity
+ sage: degree_of_regularity.quadratic_system(10, 15)
+ 4
+ sage: degree_of_regularity.quadratic_system(10, 15, q=2)
+ 3
+ sage: degree_of_regularity.quadratic_system(15, 15)
+ 16
+ """
+ return generic_system(n, [2] * m, q=q)
diff --git a/tests/module/multivariate_quadratic_estimator/mpkc/mq_estimator.py b/tests/module/multivariate_quadratic_estimator/mpkc/mq_estimator.py
new file mode 100644
index 00000000..9ac0ed83
--- /dev/null
+++ b/tests/module/multivariate_quadratic_estimator/mpkc/mq_estimator.py
@@ -0,0 +1,328 @@
+# *****************************************************************************
+# Multivariate Quadratic (MQ) Estimator
+# Copyright (C) 2021-2022 Emanuele Bellini, Rusydi H. Makarim, Javier Verbel
+# Cryptography Research Centre, Technology Innovation Institute LLC
+#
+# This file is part of MQ Estimator
+#
+# MQ Estimator 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.
+#
+# MQ Estimator 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
+# MQ Estimator. If not, see .
+# *****************************************************************************
+
+
+import inspect
+from prettytable import PrettyTable
+from sage.functions.log import log
+from .algorithms.base import BaseAlgorithm
+from .algorithms import HybridF5
+from .utils import ngates, nbits, truncate
+
+
+class MQEstimator(object):
+ """
+ Construct an instance of MQ Estimator
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``m`` -- no. of polynomials
+ - ``q`` -- order of the finite field (default: None)
+ - ``w`` -- linear algebra constant (default: 2)
+ - ``theta`` -- bit complexity exponent (default: 0)
+ - ``h`` -- external hybridization parameter (default: 0)
+ - ``nsolutions`` -- no. of solutions (default: 1)
+ - ``excluded_algorithms`` -- a list/tuple of excluded algorithms (default: None)
+
+ EXAMPLES::
+
+ sage: from mpkc import MQEstimator
+ sage: MQEstimator(n=10, m=5)
+ MQ Estimator for system with 10 variables and 5 equations
+ """
+ def __init__(self, n, m, q=None, w=2, theta=0, h=0, nsolutions=1, **kwargs):
+ constructor_args = {arg: value for (arg, value) in locals().items()
+ if arg in ('n', 'm', 'q', 'w', 'h', 'nsolutions')}
+
+ excluded_algorithms = kwargs.get("excluded_algorithms", tuple())
+ if excluded_algorithms and any(not issubclass(Algorithm, BaseAlgorithm) for Algorithm in excluded_algorithms):
+ raise TypeError(f"all excluded algorithms must be a subclass of {BaseAlgorithm.__name__}")
+
+ self._algorithms = []
+ included_algorithms = (Algorithm for Algorithm in BaseAlgorithm.__subclasses__()
+ if Algorithm not in excluded_algorithms)
+
+ for Algorithm in included_algorithms:
+ alg_constructor_args = inspect.getargs(Algorithm.__init__.__code__).args
+ arg_and_values = {arg: constructor_args[arg] for arg in alg_constructor_args
+ if arg in constructor_args and arg != 'self'}
+
+ try:
+ algorithm = Algorithm(**arg_and_values)
+ except (ValueError, TypeError):
+ continue
+
+ if algorithm.is_defined_over_finite_field() and q != algorithm.order_of_the_field():
+ continue
+
+ self._algorithms.append(algorithm)
+ self._q = q
+ self._theta = theta
+ self._h = h
+ setattr(self, algorithm.__module__.split('.')[-1], algorithm)
+
+ def algorithms(self):
+ """
+ Return a list of considered algorithms
+
+ EXAMPLES::
+
+ sage: from mpkc import MQEstimator
+ sage: E = MQEstimator(n=10, m=15, q=2)
+ sage: E.algorithms()
+ [Complexity estimator for F5 with 10 variables and 15 polynomials,
+ Complexity estimator for hybrid approach with 10 variables and 15 polynomials,
+ Dinur's first estimator for the MQ problem,
+ Dinur's second estimator for the MQ problem,
+ Exhaustive search estimator for the MQ problem,
+ BjΓΆrklund et al.'s estimator for the MQ problem,
+ Lokshtanov et al.'s estimator for the MQ problem,
+ BooleanSolve and FXL estimators for the MQ problem,
+ Crossbred estimator for the MQ problem]
+
+ TESTS::
+
+ sage: E = MQEstimator(n=10, m=15, q=3)
+ sage: E.algorithms()
+ [Complexity estimator for F5 with 10 variables and 15 polynomials,
+ Complexity estimator for hybrid approach with 10 variables and 15 polynomials,
+ Exhaustive search estimator for the MQ problem,
+ Lokshtanov et al.'s estimator for the MQ problem,
+ BooleanSolve and FXL estimators for the MQ problem,
+ Crossbred estimator for the MQ problem]
+ """
+ return self._algorithms
+
+ def algorithm_names(self):
+ """
+ Return a list of the name of considered algorithms
+
+ EXAMPLES::
+
+ sage: from mpkc import MQEstimator
+ sage: E = MQEstimator(n=10, m=15, q=2)
+ sage: E.algorithm_names()
+ ['F5',
+ 'HybridF5',
+ 'DinurFirst',
+ 'DinurSecond',
+ 'ExhaustiveSearch',
+ 'Bjorklund',
+ 'Lokshtanov',
+ 'BooleanSolveFXL',
+ 'Crossbred']
+
+ TESTS::
+
+ sage: E = MQEstimator(n=10, m=15, q=3)
+ sage: E.algorithm_names()
+ ['F5',
+ 'HybridF5',
+ 'ExhaustiveSearch',
+ 'Lokshtanov',
+ 'BooleanSolveFXL',
+ 'Crossbred']
+ """
+ return [algorithm.__class__.__name__ for algorithm in self.algorithms()]
+
+ def nalgorithms(self):
+ """
+ Return the number of considered algorithms
+
+ EXAMPLES::
+
+ sage: from mpkc import MQEstimator
+ sage: E0 = MQEstimator(n=10, m=15, q=2)
+ sage: E0.nalgorithms()
+ 9
+ sage: E1 = MQEstimator(n=183, m=12, q=4)
+ sage: E1.nalgorithms()
+ 9
+
+ TESTS::
+
+ sage: E = MQEstimator(n=10, m=15, q=3)
+ sage: E.nalgorithms()
+ 6
+ """
+ return len(self.algorithms())
+
+ def table(self, use_tilde_o_time=False, precision=3):
+ """
+ Return the table describing the complexity of each algorithm and its optimal parameters
+
+ INPUT:
+
+ - ``use_tilde_o_time`` -- use Ε time complexity (default: False)
+ - ``precision`` -- number of decimal places in the complexities exponent
+
+ EXAMPLES::
+
+ sage: from mpkc import MQEstimator
+ sage: E = MQEstimator(n=15, m=15, q=2)
+ sage: table = E.table()
+ sage: print(table)
+ +------------------+--------+--------+---------------------------+
+ | algorithm | time | memory | parameters |
+ +------------------+--------+--------+---------------------------+
+ | F5 | 27.747 | 23.158 | |
+ | HybridF5 | 21.076 | 3.906 | k: 14 |
+ | DinurFirst | 32.111 | 21.493 | Ξ»: 1/14, ΞΊ: 1/7 |
+ | DinurSecond | 20.349 | 15.801 | n1: 2 |
+ | ExhaustiveSearch | 17.966 | 11.72 | |
+ | Bjorklund | 42.451 | 15.316 | Ξ»: 1/5 |
+ | Lokshtanov | 67.123 | 16.105 | Ξ΄: 1/15 |
+ | BooleanSolveFXL | 20.339 | 5.825 | k: 14, variant: las_vegas |
+ | Crossbred | 17.672 | 16.785 | D: 4, k: 9, d: 1 |
+ +------------------+--------+--------+---------------------------+
+
+
+ TESTS::
+
+ sage: E = MQEstimator(n=15, m=15, q=3) # DinurFirst, DinurSecond, and Bjorklund are skipped for q != 2
+ sage: print(E.table())
+ +------------------+--------+--------+---------------------------+
+ | algorithm | time | memory | parameters |
+ +------------------+--------+--------+---------------------------+
+ | F5 | 35.362 | 30.484 | |
+ | HybridF5 | 28.541 | 8.55 | k: 10 |
+ | ExhaustiveSearch | 24.076 | 11.72 | |
+ | Lokshtanov | 98.227 | 24.266 | Ξ΄: 1/15 |
+ | BooleanSolveFXL | 28.529 | 5.711 | k: 14, variant: las_vegas |
+ | Crossbred | 23.36 | 22.091 | D: 5, k: 7, d: 1 |
+ +------------------+--------+--------+---------------------------+
+
+
+ sage: from mpkc.algorithms import F5, HybridF5
+ sage: E = MQEstimator(n=15, m=15, q=3, excluded_algorithms=[F5, HybridF5]) # tests excluded algorithms
+ sage: print(E.table())
+ +------------------+--------+--------+---------------------------+
+ | algorithm | time | memory | parameters |
+ +------------------+--------+--------+---------------------------+
+ | ExhaustiveSearch | 24.076 | 11.72 | |
+ | Lokshtanov | 98.227 | 24.266 | Ξ΄: 1/15 |
+ | BooleanSolveFXL | 28.529 | 5.711 | k: 14, variant: las_vegas |
+ | Crossbred | 23.36 | 22.091 | D: 5, k: 7, d: 1 |
+ +------------------+--------+--------+---------------------------+
+ """
+ table = PrettyTable()
+ table.field_names = ['algorithm', 'time', 'memory', 'parameters']
+
+ #h = self._h
+ for algorithm in self.algorithms():
+ name = algorithm.__class__.__name__
+ time_complexity = algorithm.tilde_o_time() if use_tilde_o_time else algorithm.time_complexity()
+ memory_complexity = algorithm.memory_complexity()
+ optimal_parameters = ', '.join([f"{k}: {v}" for k, v in algorithm.optimal_parameters().items()])
+
+ # time_complexity *= 2 ** h
+ if self._q is not None and self._theta > 0:
+ time_complexity = ngates(self._q, time_complexity, theta=self._theta)
+ memory_complexity = nbits(self._q, memory_complexity)
+
+ table.add_row([name,
+ truncate(log(time_complexity, 2), precision),
+ truncate(log(memory_complexity, 2), precision),
+ optimal_parameters])
+
+ return table
+
+ def fastest_algorithm(self, use_tilde_o_time=False):
+ """
+ Return the algorithm with the smallest time complexity
+
+ INPUT:
+
+ - ``use_tilde_o_time`` -- use Ε time complexity (default: False)
+
+ EXAMPLES::
+
+ sage: from mpkc import MQEstimator
+ sage: E = MQEstimator(n=15, m=15, q=2)
+ sage: E.fastest_algorithm()
+ Crossbred estimator for the MQ problem
+ """
+ key = lambda algorithm: algorithm.tilde_o_time() if use_tilde_o_time else algorithm.time_complexity()
+ return min(self.algorithms(), key=key)
+
+ def __repr__(self):
+ algorithm = self.algorithms()[0]
+ n = algorithm.nvariables()
+ m = algorithm.npolynomials()
+ return f"MQ Estimator for system with {n} variables and {m} equations"
+
+
+def min_npolynomials(security_level, q, w=2):
+ """
+ Return a minimum number of equations in a determined system that satisfies the given security level
+
+ INPUT:
+
+ - ``security_level`` -- the intended security level (in bits) (80/100/128/192/256)
+ - ``q`` -- order of the finite field
+ - ``w`` -- linear algebra constant (2 <= w <= 3) (default: 2)
+
+ EXAMPLES::
+
+ sage: from mpkc.mq_estimator import min_npolynomials
+ sage: min_npolynomials(security_level=80, q=16)
+ 31
+
+ TESTS::
+
+ sage: min_npolynomials(security_level=80, q=31)
+ 30
+ sage: min_npolynomials(security_level=80, q=256)
+ 26
+ sage: min_npolynomials(security_level=100, q=16)
+ 40
+ sage: min_npolynomials(security_level=100, q=31)
+ 38
+ sage: min_npolynomials(security_level=100, q=256)
+ 35
+ """
+ if security_level not in (80, 100, 128, 192, 256):
+ raise ValueError("the valid parameter for security_level is {80, 100, 128, 192, 256}")
+
+ m = 2
+ while log(HybridF5(n=m, m=m, q=q, w=w).time_complexity(), 2) < security_level:
+ m += 1
+
+ return m
+
+
+def min_nvariables(security_level, q, w=2):
+ """
+ Return a minimum number of variables in a determined system that satisfies the given security level
+
+ INPUT:
+
+ - ``security_level`` -- the intended security level (in bits) (80/100/128/192/256)
+ - ``q`` -- order of the finite field
+ - ``w`` -- linear algebra constant (2 <= w <= 3) (default: 2)
+
+ EXAMPLES::
+
+ sage: from mpkc.mq_estimator import min_nvariables
+ sage: min_nvariables(security_level=80, q=16)
+ 31
+ """
+ return min_npolynomials(security_level, q, w)
diff --git a/tests/module/multivariate_quadratic_estimator/mpkc/series/__init__.py b/tests/module/multivariate_quadratic_estimator/mpkc/series/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/module/multivariate_quadratic_estimator/mpkc/series/hilbert.py b/tests/module/multivariate_quadratic_estimator/mpkc/series/hilbert.py
new file mode 100644
index 00000000..e8ad9693
--- /dev/null
+++ b/tests/module/multivariate_quadratic_estimator/mpkc/series/hilbert.py
@@ -0,0 +1,173 @@
+# *****************************************************************************
+# Multivariate Quadratic (MQ) Estimator
+# Copyright (C) 2021-2022 Emanuele Bellini, Rusydi H. Makarim, Javier Verbel
+# Cryptography Research Centre, Technology Innovation Institute LLC
+#
+# This file is part of MQ Estimator
+#
+# MQ Estimator 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.
+#
+# MQ Estimator 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
+# MQ Estimator. If not, see .
+# *****************************************************************************
+
+
+from sage.all import ZZ, QQ
+from sage.misc.misc_c import prod
+from sage.rings.power_series_ring import PowerSeriesRing
+from sage.arith.misc import is_prime_power
+
+
+class HilbertSeries(object):
+ """
+ Construct an instance of Hilbert series
+
+ INPUT:
+
+ - ``n`` -- no of variables
+ - ``degrees`` -- a list of integers representing the degree of the polynomials
+ - ``q`` -- order of the finite field (default: None)
+
+ EXAMPLES::
+
+ sage: from mpkc.series.hilbert import HilbertSeries
+ sage: H = HilbertSeries(10, [2]*15)
+ sage: H
+ Hilbert series for system with 10 variables and 15 polynomials
+ sage: H = HilbertSeries(10, [2]*15, q=2)
+ sage: H
+ Hilbert series for system with 10 variables and 15 polynomials over F_2
+ """
+ def __init__(self, n, degrees, q=None):
+ self._q = q
+ self._nvariables = n
+ self._degrees = degrees
+ self._ring = PowerSeriesRing(QQ, 'z', default_prec=2*len(self._degrees))
+ z = self._ring.gen()
+ if q is not None:
+ if not is_prime_power(q):
+ raise ValueError("the order of finite field q must be a prime power")
+ if q < 2*len(self._degrees):
+ self._series = prod([(1 - z ** d) / (1 - z ** (d * q)) for d in degrees]) * ((1 - z ** q) / (1 - z)) ** n
+ else:
+ self._series = prod([1 - z ** d for d in degrees]) / (1 - z) ** n
+ else:
+ self._series = prod([1 - z ** d for d in degrees]) / (1 - z) ** n
+
+ @property
+ def nvariables(self):
+ """
+ Return the no. of variables
+
+ EXAMPLES::
+
+ sage: from mpkc.series.hilbert import HilbertSeries
+ sage: H = HilbertSeries(5, [2]*7)
+ sage: H.nvariables
+ 5
+ """
+ return self._nvariables
+
+ @property
+ def degrees(self):
+ """
+ Return a list of degrees of the polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.series.hilbert import HilbertSeries
+ sage: H = HilbertSeries(5, [2]*7)
+ sage: H.degrees
+ [2, 2, 2, 2, 2, 2, 2]
+ """
+ return self._degrees
+
+ @property
+ def precision(self):
+ """
+ Return the default precision of the series
+
+ EXAMPLES::
+
+ sage: from mpkc.series.hilbert import HilbertSeries
+ sage: H = HilbertSeries(5, [2]*7)
+ sage: H.precision
+ 14
+ """
+ return self.ring.default_prec()
+
+ @property
+ def ring(self):
+ """
+ Return the power series ring
+
+ EXAMPLES::
+
+ sage: from mpkc.series.hilbert import HilbertSeries
+ sage: H = HilbertSeries(5, [2]*7)
+ sage: H.ring
+ Power Series Ring in z over Rational Field
+ """
+ return self._ring
+
+ @property
+ def series(self):
+ """
+ Return the series
+
+ EXAMPLES::
+
+ sage: from mpkc.series.hilbert import HilbertSeries
+ sage: H = HilbertSeries(4, [2]*5)
+ sage: H.series
+ 1 + 4*z + 5*z^2 - 5*z^4 - 4*z^5 - z^6 + O(z^10)
+ sage: H = HilbertSeries(4, [2]*5, q=2)
+ sage: H.series
+ 1 + 4*z + z^2 - 16*z^3 - 14*z^4 + 40*z^5 + 50*z^6 - 80*z^7 - 125*z^8 + 140*z^9 + O(z^10)
+ """
+ return self._series
+
+ @property
+ def npolynomials(self):
+ """
+ Return the no. of polynomials
+
+ EXAMPLES::
+
+ sage: from mpkc.series.hilbert import HilbertSeries
+ sage: H = HilbertSeries(10, [2]*15)
+ sage: H.npolynomials
+ 15
+ """
+ return len(self._degrees)
+
+ def first_nonpositive_integer(self):
+ """
+ Return the first non-positive integer of the series
+
+ EXAMPLES::
+
+ sage: from mpkc.series.hilbert import HilbertSeries
+ sage: H = HilbertSeries(10, [2]*15)
+ sage: H.first_nonpositive_integer()
+ 4
+ """
+ s = self.series()
+ for d in range(self.precision):
+ if s[d] <= 0:
+ return ZZ(d)
+ else:
+ raise ValueError("unable to find a nonpositive coefficient in the series")
+
+ def __repr__(self):
+ text = f"Hilbert series for system with {self.nvariables} variables and {self.npolynomials} polynomials"
+ if self._q is not None:
+ text += f" over F_{self._q}"
+ return text
diff --git a/tests/module/multivariate_quadratic_estimator/mpkc/series/nmonomial.py b/tests/module/multivariate_quadratic_estimator/mpkc/series/nmonomial.py
new file mode 100644
index 00000000..590f7f1a
--- /dev/null
+++ b/tests/module/multivariate_quadratic_estimator/mpkc/series/nmonomial.py
@@ -0,0 +1,141 @@
+# *****************************************************************************
+# Multivariate Quadratic (MQ) Estimator
+# Copyright (C) 2021-2022 Emanuele Bellini, Rusydi H. Makarim, Javier Verbel
+# Cryptography Research Centre, Technology Innovation Institute LLC
+#
+# This file is part of MQ Estimator
+#
+# MQ Estimator 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.
+#
+# MQ Estimator 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
+# MQ Estimator. If not, see .
+# *****************************************************************************
+
+
+from sage.arith.misc import is_prime_power
+from sage.rings.power_series_ring import PowerSeriesRing
+from sage.rings.all import QQ
+
+
+class NMonomialSeries(object):
+ """
+ Construct an instance of the series of a polynomial ring
+
+ INPUT:
+
+ - ``n`` -- the number of variables
+ - ``q`` -- the size of the field (default: None)
+ - ``max_prec`` -- degree of the series (default: None)
+
+ EXAMPLES::
+
+ sage: from mpkc.series.nmonomial import NMonomialSeries
+ sage: NM = NMonomialSeries(n=6, q=5)
+ sage: NM
+ Class for the number of monomials in the polynomial ring in 6 variables over F_5
+ """
+ def __init__(self, n, q=None, max_prec=None):
+ self._n = n
+ if max_prec is not None:
+ self._max_prec = max_prec
+ else:
+ self._max_prec = self._n + 1
+
+ R = PowerSeriesRing(QQ, 'z', default_prec=self._max_prec)
+ z = R.gen()
+
+ if q is not None:
+ if not is_prime_power(q):
+ raise ValueError("the order of finite field q must be a prime power")
+ self._q = q
+ if q < self._max_prec and q <= 2 ** 10:
+ self._series_of_degree = ((1 - z ** self._q) ** self._n) / ((1 - z) ** self._n)
+ else:
+ self._series_of_degree = 1 / ((1 - z) ** self._n)
+ else:
+ self._series_of_degree = 1 / ((1 - z) ** self._n)
+
+ self._series_up_to_degree = self._series_of_degree / (1 - z)
+
+ def series_monomials_of_degree(self):
+ """
+ Return the series of the number of monomials of a given degree
+
+ EXAMPLES::
+
+ sage: from mpkc.series.nmonomial import NMonomialSeries
+ sage: NM = NMonomialSeries(n=6, q=5)
+ sage: NM.series_monomials_of_degree()
+ 1 + 6*z + 21*z^2 + 56*z^3 + 126*z^4 + 246*z^5 + 426*z^6 + O(z^7)
+ """
+ return self._series_of_degree
+
+ def series_monomials_up_to_degree(self):
+ """
+ Return the series of the number of monomials up to given degree
+
+ EXAMPLES::
+
+ sage: from mpkc.series.nmonomial import NMonomialSeries
+ sage: NM = NMonomialSeries(n=6, q=5)
+ sage: NM.series_monomials_up_to_degree()
+ 1 + 7*z + 28*z^2 + 84*z^3 + 210*z^4 + 456*z^5 + 882*z^6 + O(z^7)
+ """
+ return self._series_up_to_degree
+
+ def nmonomials_of_degree(self, d):
+ """
+ Return the number of monomials of degree `d`
+
+ INPUT:
+
+ - ``d`` -- a non-negative integer
+
+ EXAMPLES::
+
+ sage: from mpkc.series.nmonomial import NMonomialSeries
+ sage: NM = NMonomialSeries(n=6, q=5)
+ sage: NM.nmonomials_of_degree(4)
+ 126
+ """
+ max_prec = self._max_prec
+ if d < max_prec:
+ return self._series_of_degree[d]
+ else:
+ ValueError(f'The degree d should be smaller than the precision of the series which is {self._max_prec}')
+
+ def nmonomials_up_to_degree(self, d):
+ """
+ Return the number of monomials up to degree `d`
+
+ INPUT:
+
+ - ``d`` -- a non-negative integer
+
+ EXAMPLES::
+
+ sage: from mpkc.series.nmonomial import NMonomialSeries
+ sage: NM = NMonomialSeries(n=6, q=5)
+ sage: NM.nmonomials_up_to_degree(4)
+ 210
+ """
+ max_prec = self._max_prec
+ if d < max_prec:
+ return self._series_up_to_degree[d]
+ else:
+ ValueError(f'The degree d should be smaller than the precision of the series which is {max_prec}')
+
+ def __repr__(self):
+ n = self._n
+ q = self._q
+ if q is None:
+ return f'Class for the number of monomials in the polynomial ring in {n} variables'
+ else:
+ return f'Class for the number of monomials in the polynomial ring in {n} variables over F_{q}'
diff --git a/tests/module/multivariate_quadratic_estimator/mpkc/utils.py b/tests/module/multivariate_quadratic_estimator/mpkc/utils.py
new file mode 100644
index 00000000..b16cea9d
--- /dev/null
+++ b/tests/module/multivariate_quadratic_estimator/mpkc/utils.py
@@ -0,0 +1,197 @@
+# *****************************************************************************
+# Multivariate Quadratic (MQ) Estimator
+# Copyright (C) 2021-2022 Emanuele Bellini, Rusydi H. Makarim, Javier Verbel
+# Cryptography Research Centre, Technology Innovation Institute LLC
+#
+# This file is part of MQ Estimator
+#
+# MQ Estimator 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.
+#
+# MQ Estimator 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
+# MQ Estimator. If not, see .
+# *****************************************************************************
+
+
+from sage.arith.misc import is_prime_power
+from sage.functions.log import log
+from sage.functions.other import ceil
+from sage.groups.affine_gps.affine_group import AffineGroup
+from sage.rings.finite_rings.finite_field_base import is_FiniteField
+from sage.functions.other import binomial
+from .series.nmonomial import NMonomialSeries
+
+
+def ngates(q, n, theta=0):
+ """
+ Return the number of gates for the given number of multiplications in a finite field
+
+ INPUT:
+
+ - ``q`` -- order of the finite field
+ - ``n`` -- no. of multiplications
+ - ``theta`` -- exponent of the conversion factor
+
+ EXAMPLES::
+
+ sage: from mpkc.utils import ngates
+ sage: ngates(16, 2**16)
+ 2359296
+
+ TESTS::
+
+ sage: ngates(6, 2**16)
+ Traceback (most recent call last):
+ ...
+ ValueError: q must be a prime power
+ """
+ if not is_prime_power(q):
+ raise ValueError("q must be a prime power")
+ if theta:
+ return n * log(q, 2) ** theta
+ else:
+ return n * (2 * log(q, 2) ** 2 + log(q, 2))
+
+
+def nbits(q, n):
+ """
+ Return the number of bits required to store `n` elements of a finite field
+
+ - ``q`` -- order of the finite field
+ - ``n`` -- no. of field elements
+
+ EXAMPLES::
+
+ sage: from mpkc.utils import nbits
+ sage: nbits(4, 256)
+ 512
+ """
+ return ceil(log(q, 2)) * n
+
+
+def nmonomials_of_degree(d, n, q=None):
+ """
+ Return the number of `n`-variables monomials of degree `d`
+
+ .. NOTE::
+
+ If `q` is provided, then it considers the monomials in a ring modulo the ideal generated by the field equations
+
+ INPUT:
+
+ - ``d`` -- degree
+ - ``n`` -- no. of variables
+ - ``q`` -- order of finite field (default: None)
+
+ EXAMPLES::
+
+ sage: from mpkc.utils import nmonomials_of_degree
+ sage: nmonomials_of_degree(d=2, n=10)
+ 55
+ sage: nmonomials_of_degree(d=2, n=10, q=2)
+ 45
+ """
+ series = NMonomialSeries(n, q, max_prec=d+1)
+ return series.nmonomials_of_degree(d)
+
+
+def nmonomials_up_to_degree(d, n, q=None):
+ """
+ Return the number of `n`-variables monomials up to degree `d`
+
+ .. NOTE::
+
+ If `q` is provided, then it considers the monomials in a ring modulo the ideal generated by the field equations
+
+ INPUT:
+
+ - ``d`` -- degree
+ - ``n`` -- no. of variables
+ - ``q`` -- order of finite field (default: None)
+
+ EXAMPLES::
+
+ sage: from mpkc.utils import nmonomials_up_to_degree
+ sage: nmonomials_up_to_degree(d=2, n=10)
+ 66
+ sage: nmonomials_up_to_degree(d=2, n=10, q=2)
+ 56
+ """
+ series = NMonomialSeries(n, q, max_prec=d+1)
+ return series.nmonomials_up_to_degree(d)
+
+
+def random_affine_map(base_field, nvars):
+ """
+ Return a random invertible affine map
+
+ TESTS::
+
+ sage: from mpkc.utils import random_affine_map
+ sage: M, v = random_affine_map(GF(3), 4)
+ sage: M.base_ring()
+ Finite Field of size 3
+ sage: M.is_invertible()
+ True
+ sage: M.dimensions()
+ (4, 4)
+ sage: v.base_ring()
+ Finite Field of size 3
+ sage: v.length()
+ 4
+ """
+ if not is_FiniteField(base_field):
+ raise TypeError("base_field must be an instance of sage FiniteField")
+
+ if nvars < 1:
+ raise ValueError("nvars must be >= 1")
+
+ affine_map = AffineGroup(nvars, base_field).random_element()
+ matrix_ = affine_map.A()
+ vector_ = affine_map.b()
+
+ return matrix_, vector_
+
+
+def truncate(x, precision):
+ """
+ Truncates a float
+
+ INPUT:
+
+ - ``x`` -- value to be truncated
+ - ``precision`` -- number of decimal places to after which the ``x`` is truncated
+
+ EXAMPLES::
+
+ sage: from mpkc.utils import truncate
+ sage: truncate(3.2030404, 3)
+ 3.203
+ """
+ return float(int(x * 10 ** precision) / 10 ** precision)
+
+
+def sum_of_binomial_coefficients(n, l):
+ r"""
+ Return the `\sum_{j=0}^{l} \binom{n}{j}`
+
+ INPUT:
+
+ - ``n`` -- a non-negative integer
+ - ``l`` -- a non-negative integer
+
+ EXAMPLES::
+
+ sage: from mpkc.utils import sum_of_binomial_coefficients
+ sage: sum_of_binomial_coefficients(5, 2)
+ 16
+ """
+ if l < 0:
+ raise ValueError('l must be a non-negative integer')
+ return sum(binomial(n, j) for j in range(l + 1))
diff --git a/tests/module/multivariate_quadratic_estimator/mpkc/witness_degree.py b/tests/module/multivariate_quadratic_estimator/mpkc/witness_degree.py
new file mode 100644
index 00000000..20537875
--- /dev/null
+++ b/tests/module/multivariate_quadratic_estimator/mpkc/witness_degree.py
@@ -0,0 +1,75 @@
+# *****************************************************************************
+# Multivariate Quadratic (MQ) Estimator
+# Copyright (C) 2021-2022 Emanuele Bellini, Rusydi H. Makarim, Javier Verbel
+# Cryptography Research Centre, Technology Innovation Institute LLC
+#
+# This file is part of MQ Estimator
+#
+# MQ Estimator 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.
+#
+# MQ Estimator 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
+# MQ Estimator. If not, see .
+# *****************************************************************************
+
+
+from .series.hilbert import HilbertSeries
+
+
+def semi_regular_system(n, degrees, q=None):
+ """
+ Return the witness degree for semi-regular system
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``degrees`` -- a list of integers representing the degree of the polynomials
+ - ``q`` -- order of the finite field (default: None)
+
+ EXAMPLES::
+
+ sage: from mpkc import witness_degree
+ sage: witness_degree.semi_regular_system(10, [2]*15)
+ 5
+ sage: witness_degree.semi_regular_system(10, [2]*15, q=2)
+ 4
+ """
+ m = len(degrees)
+ if m <= n and q is None:
+ raise ValueError("The number of polynomials must be greater than the number of variables")
+ elif m < n and q is not None:
+ raise ValueError("The number of polynomials must be greater than or equal to the number of variables")
+
+ s = HilbertSeries(n, degrees, q=q)
+ z = s.ring.gen()
+ s._series /= (1 - z)
+ return s.first_nonpositive_integer()
+
+
+def quadratic_system(n, m, q=None):
+ """
+ Return the witness degree for quadratic system
+
+ INPUT:
+
+ - ``n`` -- no. of variables
+ - ``m`` -- no. of polynomials
+ - ``q`` -- order of the finite field (default: None)
+
+ EXAMPLES::
+
+ sage: from mpkc import witness_degree
+ sage: witness_degree.quadratic_system(10, 15)
+ 5
+ sage: witness_degree.quadratic_system(10, 15, q=2)
+ 4
+ sage: witness_degree.quadratic_system(15, 15, q=7)
+ 12
+ """
+ return semi_regular_system(n, [2] * m, q=q)
diff --git a/tests/module/optimize.py b/tests/module/optimize.py
new file mode 100644
index 00000000..9fc706ed
--- /dev/null
+++ b/tests/module/optimize.py
@@ -0,0 +1,1805 @@
+#!/usr/bin/env python3
+
+import argparse
+import collections
+import random
+import scipy.optimize as opt
+import random as rng
+from math import comb as binom
+from math import log2, log, ceil, inf
+
+# AES encryptions: kB/s
+AES = {"128": {"128": 16892817.80, "192": 14375882.78, "256": 12473219.60},
+ "256": {"128": 17863577.85, "192": 15071777.05, "256": 12982587.16}}
+McEliece_level1 = {"name": "McEliece C1", "n": 3488, "k": 2720, "w": 64}
+McEliece_level3 = {"name": "McEliece C3", "n": 4608, "k": 3360, "w": 96}
+McEliece_level5a = {"name": "McEliece C5a", "n": 6688, "k": 5024, "w": 128}
+McEliece_level5b = {"name": "McEliece C5b", "n": 6960, "k": 5413, "w": 119}
+McEliece_level5c = {"name": "McEliece C5c", "n": 8192, "k": 6528, "w": 128}
+
+BIKE_level1 = {"name": "BIKE C1", "n": 24646, "k": 12323, "w": 134, "w_k": 142}
+BIKE_level3 = {"name": "BIKE C3", "n": 49318, "k": 24659, "w": 199, "w_k": 206}
+BIKE_level5 = {"name": "BIKE C5", "n": 81946, "k": 40973, "w": 264, "w_k": 274}
+
+HQC_level1 = {"name": "HQC C1", "n": 35338, "k": 17669, "w": 132, "w_e": 75}
+HQC_level3 = {"name": "HQC C3", "n": 71702, "k": 35851, "w": 200, "w_e": 114}
+HQC_level5 = {"name": "HQC C5", "n": 115274, "k": 57637, "w": 262, "w_e": 149}
+
+levels_qc = {"BIKEmsg": [[], [], []], "BIKEkey": [[], [], []],
+ "HQC": [[], [], []]}
+AES_GATE_COUNT_LEVEL = {128: 143, 192: 207, 256: 272}
+
+NOLOG = False
+
+
+def HHH(c):
+ """
+ binary entropy
+ :param c: float in [0,1]
+ """
+ if c == 0. or c == 1.:
+ return 0.
+ if c < 0. or c > 1.:
+ return -1000
+
+ return -(c * log2(c) + (1 - c) * log2(1 - c))
+
+
+def H(c):
+ """
+ binary entropy
+ :param c: float in [0,1]
+ """
+ if c == 0. or c == 1.:
+ return 0.
+
+ if c < 0. or c > 1.:
+ return -1000
+
+ return -(c * log2(c) + (1 - c) * log2(1 - c))
+
+
+def binomH(n, k):
+ """
+ binary entropy
+ :param n: int
+ :param k: int
+ """
+ if k > n:
+ return 0
+ if k == n:
+ return 0
+ return n * HHH(k/n)
+
+
+def binomHH(n, k):
+ """
+ same as `binomH` without checks
+ """
+ return n * HHH(k/n)
+
+
+def time7diss(x):
+ """
+ magic function for the 7-dissection
+ """
+ return -2*x/3+2/3
+
+
+def time11diss(x):
+ """
+ magic function for the 11-dissection
+ """
+ return -5/4*x+3/4
+
+
+def float_range(start, stop, step):
+ """
+ helper function. Same as `range` only for floats
+ """
+ while start < stop:
+ yield float(start)
+ start += float(step)
+
+
+def check_constraints(constraints, solution):
+ """
+ checks wether constrains are fullfilled or not
+ """
+ return [(constraint['type'], constraint['fun'](solution))
+ for constraint in constraints]
+
+
+def wrap(f, g):
+ """
+ helper function injecting variables names into the optimisation process.
+ """
+ def inner(x):
+ return f(g(*x))
+ return inner
+
+
+def round_to_str(t):
+ """
+ Rounds the value 't' to a string with 4 digit precision
+ (adding trailing zeroes to emphasize precision).
+ """
+ s = str(round(t, 4))
+ # must be 6 digits
+ return (s + "0" * (5 + s.find(".") - len(s)))
+
+
+def round_upwards_to_str(t):
+ """
+ Rounds the value 't' *upwards* to a string with 4 digit precision
+ (adding trailing zeroes to emphasize precision).
+ """
+ s = str(ceil(t*10000)/10000)
+ # must be 6 digits
+ return (s + "0" * (5 + s.find(".") - len(s)))
+
+
+def xlx(x):
+ """
+ SOURCE: https://github.com/xbonnetain/optimization-subset-sum
+ """
+ if x <= 0:
+ return 0
+ return x*log2(x)
+
+
+def p_good(a0, b0, a1, b1):
+ """
+ SOURCE: https://github.com/xbonnetain/optimization-subset-sum
+ """
+ return -2*xlx(a0/2) - 2*xlx(b0/2) - xlx(a1-a0/2) - xlx(b1-b0/2) \
+ - xlx(1-a1-b1-a0/2-b0/2) - 2*g(a1, b1)
+
+
+def g(a, b):
+ """
+ SOURCE: https://github.com/xbonnetain/optimization-subset-sum
+ """
+ return -xlx(a) - xlx(b) - xlx(1-a-b)
+
+
+def f(a, b, c):
+ """
+ SOURCE: https://github.com/xbonnetain/optimization-subset-sum
+ """
+ if a <= 0:
+ return g(b, c)
+ if b <= 0:
+ return g(a, c)
+ if c <= 0:
+ return g(a, b)
+ if a+b+c >= 1:
+ return min(g(b, c), g(a, c), g(a, b))
+ try:
+ return -a*log(a, 2) - b*log(b, 2) - c*log(c, 2)\
+ - (1-a-b-c)*log(1-a-b-c, 2)
+ except:
+ return 0.
+
+
+def p_good_2(b0, a0, c0, b1, a1, c1):
+ """
+ SOURCE: https://github.com/xbonnetain/optimization-subset-sum
+ """
+ def proba(x):
+ return 2*xlx(a0/2) + 2*xlx(x+a1-a0/2-b0/2) + xlx(1-c0-2*a1-2*x)\
+ + 2*xlx(b0/2-x) + 2*xlx(x) + 2*xlx(x+c0/2-b1/2+a1/2-a0/4-b0/4)\
+ + xlx(b1-a1+a0/2+b0/2-2*x)
+
+ bounds = [(max(a0/2+b0/2-a1, 0, b1/2-a1/2+a0/4+b0/4-c0/2),
+ min(1/2.-c0/2-a1, b0/2, b1/2-a1/2+a0/4+b0/4))]
+ if bounds[0][0] > bounds[0][1]:
+ return p_good(b0, a0, b1, a1) - 1
+ return - opt.fminbound(proba, bounds[0][0], bounds[0][1], xtol=1e-15,
+ full_output=1)[1] - 2*f(a1, b1, c1)
+
+
+def p_good_2_aux(b0, a0, c0, b1, a1, c1):
+ """
+ SOURCE: https://github.com/xbonnetain/optimization-subset-sum
+ """
+ return -(2*xlx(a1-c1) + xlx(1-2*c1-2*b1) + 2*xlx(c1) + 2*xlx(b0/2-c1))\
+ - 2*f(a1, b1, c1)
+
+
+def H1(value):
+ """
+ inverse of the bin entropy function. Inverse only on [0,1] -> [0, 1/2]
+ """
+ if value == 1.0:
+ return 0.5
+
+ # approximate inverse binary entropy function
+ steps = [0.1, 0.01, 0.001, 0.0001, 0.00001, 0.000001, 0.0000001,
+ 0.00000001, 0.0000000001, 0.0000000000001, 0.000000000000001]
+ r = 0.000000000000000000000000000000001
+
+ for step in steps:
+ i = r
+ while (i + step < 1.0) and (H(i) < value):
+ i += step
+
+ r = i - step
+
+ return r
+
+
+def Hi(value):
+ """
+ puhhhh
+ """
+ return H1(value)
+
+
+###############################################################################
+#######################Adapted estimator functions#############################
+###############################################################################
+def _list_merge_async_complexity(L1, L2, l, hmap):
+ """
+ Complexity estimate of merging two lists exact
+
+ INPUT:
+
+ - ``L`` -- size of lists to be merged
+ - ``l`` -- amount of bits used for matching
+ - ``hmap`` -- indicates if hashmap is being used (Default 0: no hashmap)
+ """
+
+ if L1 == 1 and L2== 1:
+ return 1
+ if L1 == 1:
+ return L2
+ if L2 == 1:
+ return L1
+ if not hmap:
+ return 0 #to be implemented
+ else:
+ return L1+L2 + L1*L2 // 2 ** l
+
+
+def _list_merge_complexity(L, l, hmap):
+ """
+ Complexity estimate of merging two lists exact
+ INPUT:
+ - ``L`` -- size of lists to be merged
+ - ``l`` -- amount of bits used for matching
+ - ``hmap`` -- indicates if hashmap is being used (Default 0: no hashmap)
+ """
+
+ if L == 1:
+ return 1
+ if not hmap:
+ return max(1, 2 * int(log2(L)) * L + L ** 2 // 2 ** l)
+ else:
+ return 2 * L + L ** 2 // 2 ** l
+
+
+def _gaussian_elimination_complexity(n, k, r):
+ """
+ Complexity estimate of Gaussian elimination routine
+ INPUT:
+ - ``n`` -- Row additons are perfomed on ``n`` coordinates
+ - ``k`` -- Matrix consists of ``n-k`` rows
+ - ``r`` -- Blocksize of method of the four russian for inversion, default
+ is zero
+ [Bar07]_ Bard, G.V.: Algorithms for solving linear and polynomial systems
+ of equations over finite fields
+ with applications to cryptanalysis. Ph.D. thesis (2007)
+ [BLP08] Bernstein, D.J., Lange, T., Peters, C.: Attacking and defending the
+ mceliece cryptosystem.
+ In: International Workshop on Post-Quantum Cryptography. pp. 31β46.
+ Springer (2008)
+ """
+
+ if r != 0:
+ return (r ** 2 + 2 ** r + (n - k - r)) * int(((n + r - 1) / r))
+
+ return (n - k) ** 2
+
+
+def _optimize_m4ri(n, k, mem=inf):
+ """
+ Find optimal blocksize for Gaussian elimination via M4RI
+ INPUT:
+ - ``n`` -- Row additons are perfomed on ``n`` coordinates
+ - ``k`` -- Matrix consists of ``n-k`` rows
+ """
+
+ (r, v) = (0, inf)
+ for i in range(n - k):
+ tmp = log2(_gaussian_elimination_complexity(n, k, i))
+ if v > tmp and r < mem:
+ r = i
+ v = tmp
+ return r
+
+
+def _mem_matrix(n, k, r):
+ """
+ Memory usage of parity check matrix in vector space elements
+ INPUT:
+ - ``n`` -- length of the code
+ - ``k`` -- dimension of the code
+ - ``r`` -- block size of M4RI procedure
+ """
+ return n - k + 2 ** r
+
+
+###############################################################################
+##################################BCJ##########################################
+###############################################################################
+
+def optimize_bcj(new=True, only_diss=False, verb=True, membound=1.,
+ iters=10000):
+ """
+ Optimization target: original BCJ algorithm for subset-sum, using {0,-1,1}
+ representations
+ with a 4-level merging tree.
+
+ :param new: if set to true the new version of `BCJ` will be optimizes
+ if false, the original version.
+ :param only_diss: if set to `true` the optimization will take the 7/11
+ dissection into account but not the tree repetitions.
+ :param verb: verbose output
+ :param membound: set the maximal memory the optimisation should use
+ value between [0,1]
+ """
+ set_bcj = collections.namedtuple('bcj', 'l1 l2 l3 L1 L2 L3 L4 '
+ 'alpha1 alpha2 alpha3 r1 r2 r3')
+
+ if new and only_diss:
+ print("BCJ specify only the new algorithm or only dissection")
+ return
+
+ def bcj(f):
+ return wrap(f, set_bcj)
+
+ def g(a, b):
+ return -xlx(a) - xlx(b) - xlx(1-a-b)
+
+ p3 = lambda x: 1/4 + x.alpha3
+ p2 = lambda x: 1/8 + x.alpha3/2 + x.alpha2
+ p1 = lambda x: 1/16 + x.alpha3/4 + x.alpha2/2+x.alpha1
+ p0 = lambda x: p1(x)/2
+
+ m3 = lambda x: x.alpha3
+ m2 = lambda x: x.alpha3/2 + x.alpha2
+ m1 = lambda x: x.alpha3/4 + x.alpha2/2 + x.alpha1
+ m0 = lambda x: m1(x)/2
+
+ D1 = lambda x: g(p1(x), m1(x))
+ D2 = lambda x: g(p2(x), m2(x))
+ D3 = lambda x: g(p3(x), m3(x))
+ q2 = lambda x: D2(x) + x.r1 - 2*D1(x)
+ q3 = lambda x: D3(x) + x.r2 - 2*D2(x)
+ q4 = lambda x: 1 + x.r3 - 2*D3(x)
+
+ t1= lambda x: max(7*x.l1 - r(x),0)
+ t2= lambda x: max(7*x.l1 + 3*x.l2 - r(x),0) - t1(x)
+ t3= lambda x: max(7*x.l1 + 3*x.l2 + x.l3 - r(x), 0) - t1(x) - t2(x)
+
+ l = lambda x: x.l1+x.l2+x.l3
+ r = lambda x: x.r3 + 2*x.r2 + 4*x.r1
+
+ def bcj_reps(p, d, m):
+ return p+d+binomH(1-p-d, m) + binomH(1-p-d-m, m)
+
+ bjc_constraints = [
+ # representations
+ {'type': 'eq', 'fun':
+ bcj(lambda x: x.r1 - bcj_reps(p2(x), m2(x), x.alpha1))},
+ {'type': 'eq', 'fun':
+ bcj(lambda x: x.r2 - bcj_reps(p3(x), m3(x), x.alpha2))},
+ {'type': 'eq', 'fun':
+ bcj(lambda x: x.r3 - bcj_reps(1/2, 0, x.alpha3))},
+
+ # list sizes
+ # { 'type' : 'eq' , 'fun' : bcj(lambda x : x.L4 - (2*x.L3- (1-l(x)) +q4(x)))},
+ {'type': 'ineq', 'fun': bcj(lambda x: x.L3 - (2*x.L2 - x.l3 + q3(x)))},
+ {'type': 'ineq', 'fun': bcj(lambda x: x.L2 - (2*x.L1 - x.l2 + q2(x)))},
+ {'type': 'ineq', 'fun': bcj(lambda x: x.L1 - (D1(x) - x.l1))},
+
+ # correctness constraints
+ {'type': 'ineq', 'fun': bcj(lambda x: 1-l(x))},
+ # { 'type' : 'ineq', 'fun' : bcj(lambda x : x.r3-x.r2)},
+ # { 'type' : 'ineq', 'fun' : bcj(lambda x : x.r2-x.r1)},
+
+ # memory
+ {'type': 'ineq', 'fun':
+ bcj(lambda x: membound - bcj_memory(x))},
+
+ # saturation constraints
+ {'type': 'ineq', 'fun': bcj(lambda x: x.l1 - x.r1)},
+ {'type': 'ineq', 'fun':
+ bcj(lambda x: x.l2 - (2*x.r1 + x.r2 - 3*x.l1))},
+ {'type': 'ineq', 'fun': bcj(lambda x: x.l3 - (r(x)-7*x.l1-3*x.l2))},
+ ]
+
+ def bcj_classical_time_new(x):
+ """
+ Time with 7-Diss and repition of subtrees
+ """
+ x = set_bcj(*x)
+ m = max(x.L3, x.L2, x.L1)
+ space = D1(x)
+ memfac = m/space
+ memfac = max(1/7, min(memfac, 1/4))
+ timefac = time7diss(memfac)
+ return max(max(space*timefac, x.L1) + t1(x)
+ , max(x.L1, x.L2 - q2(x)) + t2(x) + t1(x)
+ , max(x.L2, x.L3 - q3(x), 2*x.L3-(1-l(x)))
+ + t3(x) + t2(x) + t1(x))
+
+ def bcj_classical_time_dissection(x):
+ """
+ Time with 7-Diss but without repition of subtrees
+ """
+ x = set_bcj(*x)
+ m = max(x.l3, x.l2, x.l1)
+ space = D1(x)
+ memfac = m/space
+ memfac = max(1/7, min(memfac, 1/4))
+ timefac = time7diss(memfac)
+ return max(max(space*timefac, x.L1)
+ , max(x.L1, x.L2 - x.l1)
+ , max(x.L2, x.L3 - x.l2)
+ , max(x.L3, x.L4 - x.l3))
+
+ def bcj_classical_time_org(x):
+ """
+ Time and Memory without 7-Diss and without repition of subtrees
+ """
+ x = set_bcj(*x)
+ return max(max(x.L1*2, x.L2 - x.l1)
+ , max(x.L2, x.L3 - x.l2)
+ , max(x.L3, x.L4 - x.l3))
+
+ def bcj_memory_new(x):
+ """
+ Memory with 7-Diss
+ """
+ x = set_bcj(*x)
+ space = D1(x)
+ m = max(x.L1, x.L2, x.L3)
+ memfac = m/space
+ memfac = max(1/7, min(memfac, 1/4))
+ return max(space*memfac, m)
+
+ def bcj_memory_org(x):
+ """
+ Memory MITM
+ """
+ x = set_bcj(*x)
+ return max(x.L4, x.L3, x.L2, x.L1)
+
+ time = bcj_classical_time_org
+ if new:
+ time = bcj_classical_time_new
+ if only_diss:
+ time = bcj_classical_time_dissection
+
+ bcj_memory = bcj_memory_org
+ if new:
+ bcj_memory = bcj_memory_new
+
+ # objective = time
+ mycons = bjc_constraints
+
+ start = [random.uniform(0, 0.15) for _ in range(7)] +\
+ [random.uniform(0, 0.05) for _ in range(3)] +\
+ [random.uniform(0, 1) for _ in range(3)]
+
+ bounds = [(0, 0.8)]*7 + [(0, 0.05)] * 3 + [(0, 1)]*3
+
+ result = opt.minimize(time, start, bounds=bounds, tol=1e-10,
+ constraints=mycons, options={'maxiter': iters})
+
+ astuple = set_bcj(*result.x)
+
+ if verb:
+ print(t1(astuple), t2(astuple), t3(astuple))
+ print("memory ", bcj_memory(result.x))
+ print("Validity: ", result.success)
+ print("Time: ", round_upwards_to_str(time(astuple)))
+ for t in astuple._asdict():
+ print(t, round_upwards_to_str(astuple._asdict()[t]))
+
+ print("Checking that the constraints are satisfied:")
+ print(check_constraints(mycons, result.x))
+ else:
+ if not NOLOG:
+ t = check_constraints(mycons, result.x)
+ valid = all(-10**(-7) <= i[1] <= 10**(-7) for i in t if i[0] == "eq")\
+ and all(-10**(-7) <= i[1] for i in t if i[0] == "ineq")
+
+ print("Validity: ", result.success & valid)
+ print("Time: ", round_upwards_to_str(time(astuple)))
+
+ t = check_constraints(mycons, result.x)
+
+ if all(-10**(-7) <= i[1] <= 10**(-7) for i in t if i[0] == "eq") \
+ and all(-10**(-7) <= i[1] for i in t if i[0] == "ineq"):
+ return time(astuple)
+ else:
+ return -1
+
+
+###############################################################################
+#################################BBSS##########################################
+###############################################################################
+
+def optimize_bbss(new=True, only_diss=False, membound=1, active_two=True,
+ verb=True, iters=10000):
+ """
+ Optimization target: new algorithm for subset-sum, using {0,-1,1,2}
+ representations
+ with a 4-level merging tree.
+
+ :param new: if set to true the new version of `BCJ` will be optimizes
+ if false, the original version.
+ :param only_diss: if set to `true` the optimization will take the 7/11
+ dissection into account but not the tree repetitions.
+ :param active_two: if set to `true` will add two into the optimization.
+ :param verb: verbose output
+ :param membound: set the maximal memory the optimisation should use
+ value between [0,1]
+ """
+ set_bbss = collections.namedtuple('classical', 'p0 p1 p2 l1 l2 l3 l4 c1 c2 c3 alpha1 alpha2 alpha3 gamma1 gamma2 gamma3')
+
+ def bbss(f):
+ return wrap(f, set_bbss)
+
+ if new and only_diss:
+ print("please specify either the ne algorithm or only dissection")
+ return
+
+ # =================================
+ # Among these variables:
+ # - pi is the filtering probability at level i
+ # - li is the list size at level i (classically, all lists at a given level
+ # have same size)
+ # (l0 = 0, since we want to find the solution)
+ # - ci is the total number of bits of the modular constraint at level i
+ # - alphai is the total number of "-1" at level i
+ # - gammai is the total number of "2" at level i
+
+ constraints_bbss = [
+ # filtering terms
+ {'type': 'eq', 'fun': bbss(lambda x: p_good_2_aux(1/2., 0, 0., 1/4.+x.alpha1-2*x.gamma1, x.alpha1, x.gamma1) - x.p0)},
+ {'type': 'eq', 'fun': bbss(lambda x: p_good_2(1/4.+x.alpha1-2*x.gamma1, x.alpha1, x.gamma1, 1/8.+x.alpha2-2*x.gamma2, x.alpha2, x.gamma2) - x.p1)},
+ {'type': 'eq', 'fun': bbss(lambda x: p_good_2(1/8.+x.alpha2-2*x.gamma2, x.alpha2, x.gamma2, 1/16.+x.alpha3-2*x.gamma3, x.alpha3, x.gamma3) - x.p2)},
+
+ # sizes of the lists
+ # { 'type' : 'eq', 'fun' : bbss(lambda x : 2*x.l1 - (1-x.c1) + x.p0 )},
+ {'type': 'eq', 'fun': bbss(lambda x: 2*x.l2 - (x.c1 - x.c2) + x.p1 - x.l1)},
+ {'type': 'eq', 'fun': bbss(lambda x: 2*x.l3 - (x.c2 - x.c3) + x.p2 - x.l2)},
+ {'type': 'eq', 'fun': bbss(lambda x: 2*x.l4 - x.c3 - x.l3)},
+ {'type': 'ineq', 'fun': bbss(lambda x: f(1/4.+x.alpha1-2*x.gamma1, x.alpha1, x.gamma1) - x.c1 - x.l1)},
+ {'type': 'ineq', 'fun': bbss(lambda x: f(1/8.+x.alpha2-2*x.gamma2, x.alpha2, x.gamma2) - x.c2 - x.l2)},
+ {'type': 'ineq', 'fun': bbss(lambda x: f(1/16.+x.alpha3-2*x.gamma3, x.alpha3, x.gamma3) - x.c3 - x.l3)},
+ {'type': 'ineq', 'fun': bbss(lambda x: x.l4-f(1/16.+x.alpha3-2*x.gamma3, x.alpha3, x.gamma3)*0.5 )},
+
+ # coherence of the -1
+ {'type': 'ineq', 'fun': bbss(lambda x: x.alpha2 - x.alpha1/2)},
+ {'type': 'ineq', 'fun': bbss(lambda x: x.alpha3 - x.alpha2/2)},
+ {'type': 'ineq', 'fun': bbss(lambda x: x.alpha1 - 2*x.gamma1)},
+ {'type': 'ineq', 'fun': bbss(lambda x: x.alpha2 - 2*x.gamma2)},
+ {'type': 'ineq', 'fun': bbss(lambda x: x.alpha3 - 2*x.gamma3)},
+
+ # memory
+ {'type': 'ineq', 'fun': bbss(lambda x: membound-bbss_memory(x))},
+ ]
+
+ if active_two:
+ constraints_bbss.append({'type': 'ineq', 'fun': bbss(lambda x: - x.gamma1)})
+ constraints_bbss.append({'type': 'ineq', 'fun': bbss(lambda x: - x.gamma2)})
+ constraints_bbss.append({'type': 'ineq', 'fun': bbss(lambda x: - x.gamma3)})
+
+ def bbss_time_new(x):
+ """
+ Time with 7-Diss and repition of subtrees
+ """
+ x = set_bbss(*x)
+ m = max(x.l3, x.l2, x.l1)
+ space = f(1/16.+x.alpha3-2*x.gamma3, x.alpha3, x.gamma3)
+ memfac = m/space
+ memfac = max(1/7, min(memfac, 1/4))
+ timefac = time7diss(memfac)
+ it3 = x.c1-x.c2 # l3
+ it2 = x.c2-x.c3 # l2
+ return max(max(space*timefac, x.l3) - min(2*x.l1 - (1-x.c1) + x.p0 + it3 + 3*it2, 0)
+ , max(x.l3, x.l2 - x.p2) - min(2*x.l1 - (1-x.c1) + x.p0 + it3, 0)
+ , max(x.l2, x.l1 - x.p1, 2*x.l1 - (1-x.c1))
+ - min(2*x.l1 - (1-x.c1) + x.p0, 0))
+
+ def bbss_time_dissection(x):
+ """
+ Time with 7-Diss but without repition of subtrees
+ """
+ x = set_bbss(*x)
+ m = max(x.l3, x.l2, x.l1)
+ space = f(1/16.+x.alpha3-2*x.gamma3, x.alpha3, x.gamma3)
+ memfac = m/space
+ memfac = max(1/7, min(memfac, 1/4))
+ timefac = time7diss(memfac)
+ # return max(x.l4, x.l3, x.l2 - x.p2, x.l1 - x.p1, -x.p0)
+ # it3=x.c1-x.c2 # l3
+ # it2=x.c2-x.c3 # l2
+ return max(max(space*timefac, x.l3) - min(2*x.l1 - (1-x.c1) + x.p0, 0)
+ , max(x.l3, x.l2 - x.p2) - min(2*x.l1 - (1-x.c1) + x.p0, 0)
+ , max(x.l2, x.l1 - x.p1, 2*x.l1 - (1-x.c1))
+ - min(2*x.l1 - (1-x.c1) + x.p0, 0))
+
+ def bbss_time_org(x):
+ """
+ Time and Memory without 7-Diss and without repition of subtrees
+ """
+ x = set_bbss(*x)
+ # it3=x.c1-x.c2 # l3
+ # it2=x.c2-x.c3 # l2
+ return max(max(x.l4, x.l3) - min(2*x.l1 - (1-x.c1) + x.p0, 0)
+ , max(x.l3, x.l2 - x.p2) - min(2*x.l1 - (1-x.c1) + x.p0, 0)
+ , max(x.l2, x.l1 - x.p1, 2*x.l1 - (1-x.c1))
+ - min(2*x.l1 - (1-x.c1) + x.p0, 0))
+
+ def bbss_memory_new(x):
+ """
+ Memory with 7-Diss
+ """
+ x = set_bbss(*x)
+ space = f(1/16.+x.alpha3-2*x.gamma3, x.alpha3, x.gamma3)
+ m = max(x.l3, x.l2, x.l1)
+ memfac = m/space
+ memfac = max(1/7, min(memfac, 1/4))
+ return max(space * memfac, x.l3, x.l2, x.l1)
+
+ def bbss_memory_org(x):
+ """
+ MITM Memory
+ """
+ x = set_bbss(*x)
+ return max(x.l4, x.l3, x.l2, x.l1)
+
+ time = bbss_time_org
+ if new:
+ time = bbss_time_new
+ if only_diss:
+ time = bbss_time_dissection
+
+ bbss_memory = bbss_memory_org
+ if new:
+ bbss_memory = bbss_memory_new
+ mycons = constraints_bbss
+
+ start = [(-0.2)]*3 +\
+ [random.uniform(0.18, 0.22) for _ in range(7)] +\
+ [0.03]*3 +\
+ [(0.000)]*3
+
+ bounds = [(-0.6, 0)]*3 +\
+ [(0, 1)]*7 +\
+ [(0, 0.05)]*3 +\
+ [(0, 0.00)]*3
+
+ result = opt.minimize(time, start, bounds=bounds, tol=1e-10,
+ constraints=mycons, options={'maxiter': iters})
+
+ astuple = set_bbss(*result.x)
+ if verb:
+ print("memory ", bbss_memory(result.x))
+ print("Validity: ", result.success)
+ print("Time: ", round_upwards_to_str(time(astuple)))
+ for t in astuple._asdict():
+ print(t, round_upwards_to_str(astuple._asdict()[t]))
+ print("Checking that the constraints are satisfied:")
+ print(check_constraints(mycons, result.x))
+ else:
+ if not NOLOG:
+ t = check_constraints(mycons, result.x)
+ valid = all(-10**(-7) <= i[1] <= 10**(-7) for i in t if i[0] == "eq")\
+ and all(-10**(-7) <= i[1] for i in t if i[0] == "ineq")
+
+ print("Validity: ", result.success & valid)
+ print("Time: ", round_upwards_to_str(time(astuple)))
+ t = check_constraints(mycons, result.x)
+
+ if all(-10**(-7) <= i[1] <= 10**(-7) for i in t if i[0] == "eq") and \
+ all(-10**(-7) <= i[1] for i in t if i[0] == "ineq"):
+ return time(astuple)
+ else:
+ return -1
+
+
+def BCJ_BBSS_memlimit(bbss=False, new=False):
+ """
+ calc runtime for every memory limitation
+ :param bbss: if set to `true` bbss will get optimized, else bcj
+ :param new: if set to `true` the new versions get optimized, else the
+ original.
+ """
+ L = []
+ algo = optimize_bcj
+ if bbss:
+ algo = optimize_bbss
+ membound = 0.29
+ while membound > 0.05:
+ mini = 2
+ c = 0
+ while c < 5:
+ try:
+ t = float(algo(new=new, verb=False))
+ print(t)
+ except ValueError:
+ print("error")
+ continue
+
+ if t != -1:
+ if mini > t:
+ mini = t
+ c += 1
+
+ L.append([membound, mini])
+ membound -= 0.02
+
+ return L
+
+
+###############################################################################
+#################################BJMM##########################################
+###############################################################################
+
+def optimize_bjmm(k=0.488, w=Hi(1-0.488)/2, verb=False, membound=1.,
+ iters=10000):
+ """
+ optimize the original version of Becker Joux May Meurers algorithm:
+ https://eprint.iacr.org/2012/026
+ :param k: code rate
+ :param w: error weight
+ :param verb: verbose output
+ :param membound: optimize under memory constraint: in [0, 1]
+ :param iters: number of iterations scipy is using.
+ """
+ set_bjmm = collections.namedtuple('BJMM', 'l p p1 p2 L0 L1 L2 r1 r2')
+
+ def bjmm(f):
+ return wrap(f, set_bjmm)
+
+ def bjmm_reps(p, p2, l):
+ if p == 0. or p2 == 0. or l == 0.:
+ return 0
+ if l < p2 or p < p2/2. or l - p2 < p - p2/2.:
+ return 0.
+
+ return binomH(p2, p2/2.) + binomH(l-p2, p-p2/2.)
+
+ def classical_time_bjmm(x):
+ x = set_bjmm(*x)
+ perms = binomHH(1., w) - binomHH(k + x.l, x.p) - \
+ binomHH(1. - k - x.l, w - x.p)
+ T1 = max(2.*x.L0 - x.r1, x.L0)
+ T2 = max(2.*x.L1 - (x.r2 - x.r1), x.L1)
+ T3 = max(2.*x.L2 - (x.l - x.r2), x.L2)
+ return perms + max(T1, T2, T3)
+
+ def classical_mem_bjmm(x):
+ x = set_bjmm(*x)
+ return max(x.L0, x.L1, x.L2)
+
+ constraints_bjmm = [
+ # weights
+ {'type': 'ineq', 'fun': bjmm(lambda x: (2. * x.p1) - x.p2)},
+ {'type': 'ineq', 'fun': bjmm(lambda x: (2. * x.p2) - x.p)},
+
+ # representations and constrains
+ {'type': 'ineq', 'fun': bjmm(lambda x: x.r2 - x.r1)},
+ {'type': 'ineq', 'fun': bjmm(lambda x: x.l - x.r2)},
+
+ # reps
+ {'type': 'eq', 'fun': bjmm(
+ lambda x: x.r1 - bjmm_reps(x.p1, x.p2, k + x.l))},
+ {'type': 'eq', 'fun': bjmm(
+ lambda x: x.r2 - bjmm_reps(x.p2, x.p, k + x.l))},
+
+ # list
+ {'type': 'eq', 'fun': bjmm(
+ lambda x: x.L0 - binomHH((k+x.l)/2, x.p1/2.))},
+ {'type': 'eq', 'fun': bjmm(
+ lambda x: x.L1 - (binomHH((k+x.l), x.p1) - x.r1))},
+ {'type': 'eq', 'fun': bjmm(
+ lambda x: x.L2 - (binomHH((k+x.l), x.p2) - x.r2))},
+
+ # memory
+ {'type': 'ineq', 'fun': bjmm(
+ lambda x: membound-classical_mem_bjmm(x))},
+ ]
+
+ time = classical_time_bjmm
+ objective = time
+ mycons = constraints_bjmm
+
+ start = [(rng.uniform(0.05, 0.09))]+[(rng.uniform(0.01, 0.02))] + \
+ [(rng.uniform(0.001, 0.015))]*2 + \
+ [(0.031)]*3 + [(rng.uniform(0.001, 0.2))]*2
+ bounds = [(0.05, 0.08)]*1 + [(0.001, 0.03)]*1 + \
+ [(0.0002, 0.02)]*2 + [(0.001, 0.04)]*3 + [(0.002, 0.05)]*2
+
+ result = opt.minimize(time, start,
+ bounds=bounds, tol=1e-7,
+ constraints=mycons, options={'maxiter': iters})
+
+ astuple = set_bjmm(*result.x)
+
+ if verb:
+ print("Validity: ", result.success)
+ print("Time: ", time(astuple))
+
+ for t in astuple._asdict():
+ print(t, round_to_str(astuple._asdict()[t]))
+ print("Checking that the constraints are satisfied:")
+ print(check_constraints(mycons, result.x))
+ else:
+ if not NOLOG:
+ t = check_constraints(mycons, result.x)
+ valid = all(-10**(-7) <= i[1] <= 10**(-7) for i in t if i[0] == "eq")\
+ and all(-10**(-7) <= i[1] for i in t if i[0] == "ineq")
+
+ print("Validity: ", result.success & valid)
+ print("Time: ", round_upwards_to_str(time(astuple)))
+
+ return result.success, objective(astuple), result
+
+
+def optimize_mem_bjmm(retries=1000):
+ """
+ finds the optimal runtime under a certain memory restriction
+ :param retries: number of repetitions to find the minimum. Note that this
+ values does NOT effect the number of repetitions of scipy.
+ """
+ bjmm_data = []
+ for mem in float_range(0., 0.2, 0.1):
+ global bjmm_membound
+ bjmm_membound = mem
+ time = min([optimize_bjmm(verb=False, membound=bjmm_membound) for _ in range(retries)])
+ bjmm_data.append([mem, time])
+ print(bjmm_data)
+ return bjmm_data
+
+
+def optimize_k_bjmm(retries=1000, half_distance=True):
+ """
+ optimize under restrictions on the code rate
+ :param retries: number of repetitions to find the minimum. Note that this
+ values does NOT effect the number of repetitions of scipy.
+ """
+ bjmm_data = []
+ for k_ in float_range(0., 0.5, 0.1):
+ k = k_
+ w = Hi(1 - k_)
+ if half_distance:
+ w = w/2
+
+ time = min([optimize_bjmm(k=k, w=w, verb=False) for _ in range(retries)])
+ bjmm_data.append([k, w, time])
+ print(bjmm_data)
+ return bjmm_data
+
+
+###############################################################################
+#################################NEW BJMM######################################
+###############################################################################
+
+
+def optimize_new_bjmm(k=0.488, w=Hi(1-0.488)/2, verb=False,
+ membound=1., iters=10000):
+ """
+ optimize the new version of Becker Joux May Meurers algorithm:
+ https://eprint.iacr.org/2012/026
+ :param k: code rate
+ :param w: error weight
+ :param verb: verbose output
+ :param membound: optimize under memory constraint: in [0, 1]
+ :param iters: number of iterations scipy is using.
+ """
+
+ set_new_bjmm = collections.namedtuple(
+ 'NEWBJMM', 'l p p1 p2 L1 L2 L3 r1 r2 l1 l2')
+
+ def new_bjmm(f):
+ return wrap(f, set_new_bjmm)
+
+ def new_bjmm_reps(p, p2, l):
+ if p == 0. or p2 == 0. or l == 0.:
+ return 0
+ if l < p2 or p < p2/2. or l - p2 < p - p2/2.:
+ return 0.
+
+ return binomHH(p2, p2/2.) + binomHH(l-p2, p-p2/2.)
+
+ def classical_time_new_bjmm(x):
+ x = set_new_bjmm(*x)
+ perms = binomHH(1., w) - binomHH(k + x.l, x.p) - \
+ binomHH(1. - k - x.l, w - x.p)
+ T1 = max(2*x.L1 - x.l1, x.L1)
+ T2 = max(2*x.L2 - x.r2, x.L2)
+ T3 = max(2*x.L3 - (x.l - x.l2 - x.l1), x.L3)
+
+ return max(
+ max(3*x.l1 - 2*x.r1 - x.r2, 0) + T1,
+ max(3*x.l1 + x.l2 - 2*x.r1 - x.r2, 0) + max(T2, T3)
+ ) + perms
+ # return perms + max(x.L1,
+ # 2*x.L1-x.r1, x.L2,
+ # 2*x.L2-(x.r2-x.r1),
+ # x.L3,
+ # 2*x.L3 - (x.l - x.r2))
+ # #x.L4)
+
+ def classical_mem_new_bjmm(x):
+ x = set_new_bjmm(*x)
+ return max(x.L1/2, x.L2, x.L3) # , x.L4)
+
+ constraints_new_bjmm = [
+ # weights
+ {'type': 'ineq', 'fun': new_bjmm(lambda x: 2. * x.p1 - x.p2)},
+ {'type': 'ineq', 'fun': new_bjmm(lambda x: 2. * x.p2 - x.p)},
+
+ # reps
+ {'type': 'eq', 'fun': new_bjmm(
+ lambda x: x.r1 - new_bjmm_reps(x.p1, x.p2, k + x.l))},
+ {'type': 'eq', 'fun': new_bjmm(
+ lambda x: x.r2 - new_bjmm_reps(x.p2, x.p, k + x.l))},
+
+ # binomial coeeficient correctness
+ {'type': 'ineq', 'fun': new_bjmm(lambda x: x.l - (x.l1 + x.l2))},
+ {'type': 'ineq', 'fun': new_bjmm(lambda x: x.l1 - x.r1)},
+ {'type': 'ineq', 'fun': new_bjmm(lambda x: x.l2 - x.r2)},
+ # { 'type' : 'ineq', 'fun' : new_bjmm(lambda x : 2*x.r1 + x.r2 - x.l2 - 3*x.l1) },
+
+ # list
+ {'type': 'eq', 'fun': new_bjmm(
+ lambda x: x.L1 - binomHH((k+x.l)/2, x.p1/2.))},
+ {'type': 'eq', 'fun': new_bjmm(
+ lambda x: x.L2 - (binomHH((k+x.l), x.p1) - x.l1))},
+ {'type': 'eq', 'fun': new_bjmm(
+ lambda x: x.L3 - (binomHH((k+x.l), x.p2) + x.r1 - 3*x.l1 - x.l2))},
+ # { 'type' : 'eq', 'fun' : new_bjmm(lambda x : x.L4 - (binomHH((k+x.l), x.p) - x.l) ) },
+
+ # correctness
+ # { 'type' : 'ineq', 'fun' : new_bjmm(lambda x : 1. - k - x.l) },
+ # { 'type' : 'ineq', 'fun' : new_bjmm(lambda x : (1. - k - x.l) - (w - x.p)) },
+ # { 'type' : 'ineq', 'fun' : new_bjmm(lambda x : w - x.p) },
+
+ # memory
+ {'type': 'ineq', 'fun': new_bjmm(
+ lambda x: membound-classical_mem_new_bjmm(x))},
+ ]
+
+ time = classical_time_new_bjmm
+ objective = time
+ mycons = constraints_new_bjmm
+
+ start = [(rng.uniform(0.06, 0.08))] + \
+ [(rng.uniform(0.01, 0.02))] + \
+ [(rng.uniform(0.005, 0.2))]*9
+ bounds = [(0.06, 0.08)]*1 + \
+ [(0.01, 0.02)]*1 + \
+ [(0.0002, 0.02)]*2 + \
+ [(0.001, 0.04)]*7
+
+ result = opt.minimize(time, start,
+ bounds=bounds, tol=1e-7,
+ constraints=mycons, options={'maxiter': iters})
+
+ astuple = set_new_bjmm(*result.x)
+ if verb:
+ print("Validity: ", result.success)
+ print("Time: ", time(astuple))
+
+ for t in astuple._asdict():
+ print(t, round_to_str(astuple._asdict()[t]))
+
+ print("Checking that the constraints are satisfied:")
+ print(check_constraints(mycons, result.x))
+ else:
+ if not NOLOG:
+ t = check_constraints(mycons, result.x)
+ valid = all(-10**(-7) <= i[1] <= 10**(-7) for i in t if i[0] == "eq")\
+ and all(-10**(-7) <= i[1] for i in t if i[0] == "ineq")
+
+ print("Validity: ", result.success & valid)
+ print("Time: ", round_upwards_to_str(time(astuple)))
+
+ return result.success, objective(astuple), result
+
+
+def optimize_mem_new_bjmm(retries=1000):
+ """
+ finds the optimal runtime under a certain memory restriction
+ :param retries: number of repetitions to find the minimum. Note that this
+ values does NOT effect the number of repetitions of scipy.
+ """
+ bjmm_data = []
+ for mem in float_range(0., 0.2, 0.02):
+ global new_bjmm_membound
+ new_bjmm_membound = mem
+ time = min([optimize_new_bjmm(membound=new_bjmm_membound, verb=False) for _ in range(retries)])
+ bjmm_data.append([mem, time])
+ print(bjmm_data)
+ return bjmm_data
+
+
+def optimize_k_new_bjmm(retries=1000, half_distance=True):
+ """
+ optimize under restrictions on the code rate
+ :param retries: number of repetitions to find the minimum. Note that this
+ values does NOT effect the number of repetitions of scipy.
+ """
+ bjmm_data = []
+ for k_ in float_range(0., 0.5, 0.1):
+ k = k_
+ w = Hi(1 - k_)
+ if half_distance:
+ w = w/2
+
+ time = min([optimize_new_bjmm(k=k, w=w, verb=False) for _ in range(retries)])
+ bjmm_data.append([k, w, time])
+ print(bjmm_data)
+ return bjmm_data
+
+
+###############################################################################
+#################################MMT###########################################
+###############################################################################
+
+
+def optimize_mmt(k=0.488, w=Hi(1-0.488)/2, verb=False, membound=1.,
+ iters=10000):
+ """
+ optimize the original version of May Meurers Thomae algorithm:
+ https://www.iacr.org/archive/asiacrypt2011/70730106/70730106.pdf
+ :param k: code rate
+ :param w: error weight
+ :param verb: verbose output
+ :param membound: optimize under memory constraint: in [0, 1]
+ :param iters: number of iterations scipy is using.
+ """
+ set_mmt = collections.namedtuple('MMT', 'l p L1 L2 L3 r1')
+
+ def mmt(f):
+ return wrap(f, set_mmt)
+
+ def dissection_mmt_memory(x):
+ """Memory with 7-Diss
+ """
+ x = set_mmt(*x)
+ space = binomHH((k+x.l)/2, x.p/4)
+ m = max(x.L1, x.L2, x.L3)
+ memfac = m / space
+ memfac = max(1/7, min(memfac, 1/4))
+ return max(space * memfac, m)
+
+ def classical_time_mmt(x):
+ """
+ """
+ x = set_mmt(*x)
+ perms = binomHH(1., w) - binomHH(k + x.l, x.p) - \
+ binomHH(1. - k - x.l, w - x.p)
+ return perms + max(x.L1, x.L2, x.L3)
+
+ def classical_memory_mmt(x):
+ """
+ """
+ return max(x.L1, x.L2, x.L3)
+ # return max(x.L1/2, x.L2, x.L3)
+ # return dissection_mmt_memory(x)
+
+ # classical mmt
+ constraints_mmt = [
+ # list
+ {'type': 'eq', 'fun': mmt(lambda x: x.L1 - binomHH((k+x.l)/2, x.p/4))},
+ {'type': 'eq', 'fun': mmt(lambda x: x.L2 - (2*x.L1 - x.r1))},
+ {'type': 'eq', 'fun': mmt(lambda x: x.L3 - (2*x.L2 - (x.l - x.r1)))},
+ # reps
+ {'type': 'eq', 'fun': mmt(lambda x: x.r1 - binomHH(x.p, x.p/2.))},
+ # memory
+ {'type': 'ineq', 'fun': mmt(
+ lambda x: membound-classical_memory_mmt(x))},
+ ]
+
+ start = [(rng.uniform(0.001, 0.2))]*2 +\
+ [(0.1)] * 3 + \
+ [(rng.uniform(0.001, 0.22))]*1
+ bounds = [(0., 0.3)]*6
+
+ result = opt.minimize(classical_time_mmt, start,
+ bounds=bounds, tol=1e-7,
+ constraints=constraints_mmt, options={'maxiter': iters})
+
+ astuple = set_mmt(*result.x)
+
+ if verb:
+ print("Validity: ", result.success)
+ print("Time: ", classical_time_mmt(astuple))
+
+ for t in astuple._asdict():
+ print(t, round_to_str(astuple._asdict()[t]))
+ print("Checking that the constraints are satisfied:")
+ print(check_constraints(constraints_mmt, result.x))
+
+ return result.success, classical_time_mmt(astuple), result
+ else:
+ if not NOLOG:
+ t = check_constraints(constraints_mmt, result.x)
+ valid = all(-10**(-7) <= i[1] <= 10**(-7) for i in t if i[0] == "eq")\
+ and all(-10**(-7) <= i[1] for i in t if i[0] == "ineq")
+
+ print("Validity: ", result.success & valid)
+ print("Time: ", round_upwards_to_str(classical_time_mmt(astuple)))
+
+ return classical_time_mmt(astuple)
+
+
+def optimize_mem_mmt(retries=1000):
+ """
+ finds the optimal runtime under a certain memory restriction
+ :param retries: number of repetitions to find the minimum. Note that this
+ values does NOT effect the number of repetitions of scipy.
+ """
+ mmt_data = []
+ for mem in float_range(0., 0.2, 0.02):
+ time = min([optimize_mmt(membound=mem, verb=False) for _ in range(retries)])
+ mmt_data.append([mem, time])
+ print(mmt_data)
+ return mmt_data
+
+
+def optimize_k_mmt(retries=1000, half_distance=True):
+ """
+ optimize under restrictions on the code rate
+ :param retries: number of repetitions to find the minimum. Note that this
+ values does NOT effect the number of repetitions of scipy.
+ """
+ mmt_data = []
+ for k_ in float_range(0., 0.5, 0.1):
+ k = k_
+ w = Hi(1 - k_)
+ if half_distance:
+ w = w/2
+
+ time = min([optimize_mmt(k=k, w=w, verb=False) for _ in range(retries)])
+ mmt_data.append([k, w, time])
+ print(mmt_data)
+ return mmt_data
+
+
+def optimize_new_mmt(k=0.488, w=Hi(1-0.488)/2, verb=False,
+ membound=1.0, iters=10000):
+ """
+ optimize the new version of May Meurers Thomae algorithm:
+ https://www.iacr.org/archive/asiacrypt2011/70730106/70730106.pdf
+ :param k: code rate
+ :param w: error weight
+ :param verb: verbose output
+ :param membound: optimize under memory constraint: in [0, 1]
+ :param iters: number of iterations scipy is using.
+ """
+ set_new_mmt = collections.namedtuple('NEWMMT', 'l p L1 L2 L3 r1 l1')
+
+ def new_mmt(f):
+ return wrap(f, set_new_mmt)
+
+ def dissection_new_mmt_memory(x):
+ """
+ Memory with 7-Diss
+ """
+ x = set_new_mmt(*x)
+ space = binomHH((k+x.l)/2, x.p/4)
+ m = max(x.L1, x.L2, x.L3)
+ memfac = m / space
+ memfac = max(1/7, min(memfac, 1/4))
+ return max(space * memfac, m)
+
+ def classical_time_new_mmt(x):
+ """
+ """
+ x = set_new_mmt(*x)
+ perms = binomHH(1., w) - binomHH(k + x.l, x.p) - \
+ binomHH(1. - k - x.l, w - x.p)
+ tree = max(x.L1, x.L2, x.L3) + max(0, x.l1-x.r1)
+ return perms + tree
+
+ def classical_memory_new_mmt(x):
+ """
+ """
+ x = set_new_mmt(*x)
+ return max(x.L1, x.L2, x.L3)
+ # return max(x.L1/2, x.L2, x.L3)
+ # return dissection_genius_mmt_memory(x)
+
+ constraints_new_mmt = [
+ # list
+ {'type': 'eq', 'fun': new_mmt(
+ lambda x: x.L1 - binomHH((k+x.l)/2, x.p/4))},
+ {'type': 'eq', 'fun': new_mmt(lambda x: x.L2 - (2*x.L1 - x.l1))},
+ {'type': 'eq', 'fun': new_mmt(lambda x: x.L3 - (2*x.L2 - (x.l - x.l1)))},
+ # reps
+ {'type': 'eq', 'fun': new_mmt(lambda x: x.r1 - binomHH(x.p, x.p/2.))},
+ # memory
+ {'type': 'ineq', 'fun': new_mmt(
+ lambda x: membound-classical_memory_new_mmt(x))},
+ # correctness
+ {'type': 'ineq', 'fun': new_mmt(lambda x: x.l - x.l1)},
+ {'type': 'ineq', 'fun': new_mmt(lambda x: x.l1 - x.r1)},
+ ]
+
+ start = [(rng.uniform(0.0, 0.03))]*1 + [(rng.uniform(0.0, 0.03))] * \
+ 1 + [(0.01)]*3 + [(rng.uniform(0.001, 0.01))]*2
+ # start = [(0.022)]*1 + [(0.007)]*1 + [(0.015)]*3 + [(0.007)]*2
+ bounds = [(0., 0.05)] + [(0., 0.05)] + [(0., 0.02)]*3 + [(0., 0.02)]*2
+
+ result = opt.minimize(classical_time_new_mmt, start,
+ bounds=bounds, tol=1e-7,
+ constraints=constraints_new_mmt, options={'maxiter': iters})
+
+ astuple = set_new_mmt(*result.x)
+
+ if verb:
+ print("Validity: ", result.success)
+ print("Time: ", classical_time_new_mmt(astuple))
+
+ for t in astuple._asdict():
+ print(t, round_to_str(astuple._asdict()[t]))
+ print("Checking that the constraints are satisfied:")
+ print(check_constraints(constraints_new_mmt, result.x))
+
+ return result.success, classical_time_new_mmt(astuple), result
+ else:
+ if not NOLOG:
+ t = check_constraints(constraints_new_mmt, result.x)
+ valid = all(-10**(-7) <= i[1] <= 10**(-7) for i in t if i[0] == "eq")\
+ and all(-10**(-7) <= i[1] for i in t if i[0] == "ineq")
+
+ print("Validity: ", result.success & valid)
+ print("Time: ", round_upwards_to_str(classical_time_new_mmt(astuple)))
+ return classical_time_new_mmt(astuple)
+
+
+def optimize_mem_new_mmt(retries=100):
+ """
+ finds the optimal runtime under a certain memory restriction
+ :param retries: number of repetitions to find the minimum. Note that this
+ values does NOT effect the number of repetitions of scipy.
+ """
+ mmt_data = []
+ retries = 3
+ for mem in float_range(0., 0.2, 0.02):
+ time = min([optimize_new_mmt(membound=mem, verb=False) for _ in range(retries)])
+ mmt_data.append([mem, time])
+ print(mmt_data)
+ return mmt_data
+
+
+def optimize_k_new_mmt(retries=1000, half_distance=True):
+ """
+ optimize under restrictions on the code rate
+ :param retries: number of repetitions to find the minimum. Note that this
+ values does NOT effect the number of repetitions of scipy.
+ """
+ mmt_data = []
+ retries = 3
+ for k_ in float_range(0., 0.5, 0.1):
+ k = k_
+ w = Hi(1 - k_)
+ if half_distance:
+ w = w/2
+
+ time = min([optimize_new_mmt(k=k, w=w, verb=False) for _ in range(retries)])
+ mmt_data.append([k, w, time])
+ print(mmt_data)
+ return mmt_data
+
+
+def bjmm_depth_2_qc_complexity(n: int, k: int, w: int, mem=inf, hmap=1, mmt=0, qc=0, base_p=-1, l_val=0, l1_val=0, memory_access=0, enable_tmto=1):
+ """
+ Complexity estimate of BJMM algorithm in depth 2
+ [MMT11] May, A., Meurer, A., Thomae, E.: Decoding random linear codes in 2^(0.054n). In: International Conference
+ on the Theory and Application of Cryptology and Information Security. pp. 107β124. Springer (2011)
+ [BJMM12] Becker, A., Joux, A., May, A., Meurer, A.: Decoding random binary linear codes in 2^(n/20): How 1+ 1= 0
+ improves information set decoding. In: Annual international conference on the theory and applications of
+ cryptographic techniques. pp. 520β536. Springer (2012)
+ expected weight distribution::
+ +--------------------------+-------------------+-------------------+
+ | <-----+ n - k - l +----->|<--+ (k + l)/2 +-->|<--+ (k + l)/2 +-->|
+ | w - 2p | p | p |
+ +--------------------------+-------------------+-------------------+
+
+ INPUT:
+ - ``n`` -- length of the code
+ - ``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)
+ - ``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)
+ - ``mmt`` -- restrict optimization to use of MMT algorithm (precisely enforce p1=p/2)
+ - ``qc`` -- optimize in the quasi cyclic setting
+ - ``base_p`` -- hard code the base p enumerated in the baselists.
+ if this value is set to -1 the code will optimize over
+ different values
+ - ``l_val`` -- hardcode `l`. If set to 0 the code will optimize over
+ different values.
+ - ``l1_val`` -- same as `l` only for `l1`
+ - ``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)
+ - ``enable_tmto`` -- enable the new time memory tradeoff proposed in
+ this work
+ """
+ n = int(n)
+ k = int(k)
+ w = int(w)
+
+ solutions = max(0, log2(binom(n, w)) - (n - k))
+ time = inf
+ memory = 0
+ r = _optimize_m4ri(n, k, mem)
+
+ i_val = [25, 450, 25]
+ i_val_inc = [10, 10, 10]
+ params = [-1 for _ in range(7)]
+ lists = []
+
+ while True:
+ stop = True
+ mod2 = (params[0] - i_val_inc[0]//2) % 2
+ for p in range(max(params[0] - i_val_inc[0]//2 - mod2, 2*qc), min(w // 2, i_val[0]), 2):
+ for l in range(max(params[1] - i_val_inc[1] // 2, 0), min(n - k - (w - 2 * p), min(i_val[1], n - k))):
+ for p1 in range(max(params[2] - i_val_inc[2] // 2, (p + 1) // 2, qc), min(w, i_val[2])):
+ if mmt and p1 != p // 2:
+ continue
+
+ if base_p != -1 and p1 != base_p:
+ continue
+
+ if l_val != 0 and l != l_val:
+ continue
+
+ k1 = (k + l) // 2
+ L1 = binom(k1, p1)
+ if qc:
+ L1b = binom(k1, p1-1)*k
+
+ if log2(L1) > time:
+ continue
+
+ if k1 - p < p1 - p / 2:
+ continue
+
+ if not (qc):
+ reps = (binom(p, p//2) * binom(k1 - p, p1 - p//2)) ** 2
+ else:
+ reps = binom(p, p//2) * binom(k1 - p, p1 -
+ p//2)*binom(k1 - p+1, p1 - p // 2)
+ if p-1 > p//2:
+ reps *= (binom(p-1, p // 2))
+
+ if enable_tmto == 1:
+ start = int(log2(L1))-5
+ end = start + 10
+ else:
+ start = int(ceil(log2(reps)))
+ end = start + 1
+
+ for l1 in range(start, end):
+ if l1 > l:
+ continue
+
+ L12 = max(1, L1 ** 2 // 2 ** l1)
+
+ qc_advantage = 0
+ if qc:
+ L12b = max(1, L1*L1b//2**l1)
+ qc_advantage = log2(k)
+
+ tmp_mem = log2((2 * L1 + L12) + _mem_matrix(n, k, r)) if not (
+ qc) else log2(L1+L1b + min(L12, L12b) + _mem_matrix(n, k, r))
+ if tmp_mem > mem:
+ continue
+
+ Tp = max(log2(binom(n, w))
+ - log2(binom(n - k - l, w - 2 * p + qc))
+ - log2(binom(k1, p))
+ - log2(binom(k1, p - qc))
+ - qc_advantage - solutions, 0)
+
+ Tg = _gaussian_elimination_complexity(n, k, r)
+ if not (qc):
+ T_tree = 2 * _list_merge_complexity(L1, l1, hmap) + _list_merge_complexity(L12,
+ l - l1,
+ hmap)
+ else:
+ T_tree = _list_merge_async_complexity(L1, L1b, l1, hmap) + _list_merge_complexity(L1, l1, hmap) + _list_merge_async_complexity(L12, L12b,
+ l-l1, hmap)
+
+ T_rep = int(ceil(2 ** (l1 - log2(reps))))
+
+ tmp = Tp + log2(Tg + T_rep * T_tree)
+ # print(tmp, Tp, T_rep, T_tree)
+
+ if memory_access == 1:
+ tmp += log2(tmp_mem)
+ elif memory_access == 2:
+ tmp += tmp_mem/3
+ elif callable(memory_access):
+ tmp += memory_access(tmp_mem)
+
+ if tmp < time or (tmp == time and tmp_mem < memory):
+ time = tmp
+ memory = tmp_mem
+ params = [p, l, p1, T_tree, Tp, l1, log2(reps)]
+ tree_detail = [log2(Tg), log2(
+ 2 * _list_merge_complexity(L1, l1, hmap)), log2(_list_merge_complexity(L12, l - l1, hmap))]
+ lists = [log2(L1), log2(L12), 2*log2(L12)-(l-l1)]
+
+ for i in range(len(i_val)):
+ if params[i] == i_val[i] - 1:
+ stop = False
+ i_val[i] += i_val_inc[i]
+
+ if stop == True:
+ break
+ par = {"l": params[1], "p": params[0], "p1": params[2],
+ "l1": params[5], "reps": params[6], "depth": 2}
+ res = {"time": time, "memory": memory, "parameters": par,
+ "perms": params[4], "lists": lists}
+ return res
+
+
+def compute_mceliece_table():
+ """
+ """
+ verbose = 1
+ levels_mceliece = [[], [], []]
+ for access_cost in range(3):
+ for mem_indicator in range(3):
+ levels_mceliece[access_cost].append([])
+ for sec_level in [128, 192, 256, 257, 258]:
+ switch = sec_level
+ if sec_level > 256:
+ sec_level = 256
+
+ AES_blockwidth = 128 if sec_level == 128 else 256
+ AES_kilobytes = AES[str(AES_blockwidth)][str(sec_level)]
+ if switch == 128:
+ c = McEliece_level1
+ elif switch == 192:
+ c = McEliece_level3
+ elif switch == 256:
+ c = McEliece_level5a
+ elif switch == 257:
+ c = McEliece_level5b
+ else:
+ c = McEliece_level5c
+ n = c["n"]
+ k = c["k"]-1
+ w = c["w"]
+
+ AES_kilobytes *= 2
+
+ AES_encryptions_sec = AES_kilobytes*1024*8/AES_blockwidth
+
+ AES_encryptions_year = AES_encryptions_sec*60*60*24*365
+ AES_years = sec_level-log2(AES_encryptions_year)
+
+ McEliece_1284_years = 16.01/365
+ if mem_indicator == 0:
+ max_mem = inf
+ elif mem_indicator == 1:
+ max_mem = 80-log2(n)
+ else:
+ max_mem = 60-log2(n)
+
+ McEliece_1284_complexity = bjmm_depth_2_qc_complexity(
+ 1284, 1027, 24, memory_access=access_cost, enable_tmto=1)["time"]+log2(1284)
+ McEliece_big_complexity = bjmm_depth_2_qc_complexity(
+ n, k, w, memory_access=access_cost, mem=max_mem, enable_tmto=1)["time"]+log2(n)
+
+ McEliece_big_years = log2(
+ McEliece_1284_years)+McEliece_big_complexity-McEliece_1284_complexity
+
+ if verbose:
+ if AES_years-McEliece_big_years < 0:
+ print("McEliece is", abs(
+ AES_years-McEliece_big_years), "bits harder")
+ else:
+ print("AES is", abs(
+ AES_years-McEliece_big_years), "bits harder")
+ levels_mceliece[access_cost][mem_indicator].append(
+ McEliece_big_years-AES_years)
+
+
+def compute_qc_table():
+ """
+ """
+ verbose = 1
+ levels_qc = {"BIKEmsg": [[], [], []],
+ "BIKEkey": [[], [], []], "HQC": [[], [], []]}
+ for bike_o_hqc in ["HQC", "BIKEmsg", "BIKEkey",]:
+ for access_cost in range(3):
+ for mem_indicator in range(1):
+ levels_qc[bike_o_hqc][access_cost].append([])
+ for sec_level in [128, 192, 256]:
+ AES_blockwidth = 128 if sec_level == 128 else 256
+ AES_kilobytes = AES[str(AES_blockwidth)][str(sec_level)]
+ if sec_level == 128:
+ c = params_qc[bike_o_hqc][0]
+ elif sec_level == 192:
+ c = params_qc[bike_o_hqc][1]
+ else:
+ c = params_qc[bike_o_hqc][2]
+
+ n = c["n"]
+ k = c["k"]
+ if bike_o_hqc == "BIKEkey":
+ w = c["w_k"]
+ else:
+ w = c["w"]
+
+ AES_kilobytes *= 2
+
+ AES_encryptions_sec = AES_kilobytes*1024*8/AES_blockwidth
+ AES_encryptions_year = AES_encryptions_sec*60*60*24*365
+ AES_years = sec_level-log2(AES_encryptions_year)
+
+ max_mem = inf
+ if mem_indicator == 0:
+ max_mem = inf
+ elif mem_indicator == 1:
+ max_mem = 60 - log2(n)
+ else:
+ max_mem = 80 - log2(n)
+
+ Experiment_years = 38.16/24/365
+ Experiment_complexity = bjmm_depth_2_qc_complexity(
+ 3138, 3138//2, 56, qc=1, memory_access=access_cost, enable_tmto=1)["time"]+log2(3138)
+ big_complexity = bjmm_depth_2_qc_complexity(
+ n, k, w, mem=max_mem, qc=0 if bike_o_hqc == "BIKEkey" else 1, memory_access=access_cost, enable_tmto=1)["time"]+log2(n)
+ if bike_o_hqc == "BIKEkey":
+ big_complexity -= log2(k)
+ big_years = log2(Experiment_years) + \
+ big_complexity-Experiment_complexity
+ if verbose:
+ if AES_years-big_years < 0:
+ # print(abs(AES_years-big_years))
+ name = bike_o_hqc
+ print(name, "is", abs(AES_years-big_years), "bits harder")
+ else:
+ print("AES is", abs(AES_years-big_years), "bits harder")
+ levels_qc[bike_o_hqc][access_cost][mem_indicator].append(
+ big_years-AES_years)
+
+
+def compute_mceliece_table_vs_aes(verbose=True):
+ """
+ """
+ tm = 0
+ levels_mceliece = [[], [], []]
+ for access_cost in range(3):
+ for mem_indicator in range(3):
+ levels_mceliece[access_cost].append([])
+ for sec_level in [128, 192, 256, 257, 258]:
+ switch = sec_level
+ if sec_level > 256:
+ sec_level = 256
+
+ # AES_blockwidth=128 if sec_level==128 else 256
+ # AES_kilobytes=AES[str(AES_blockwidth)][str(sec_level)]
+ if switch == 128:
+ c = McEliece_level1
+ elif switch == 192:
+ c = McEliece_level3
+ elif switch == 256:
+ c = McEliece_level5a
+ elif switch == 257:
+ c = McEliece_level5b
+ else:
+ c = McEliece_level5c
+
+ n = c["n"]
+ k = c["k"]-1
+ w = c["w"]
+
+ # AES_kilobytes*=2
+ # AES_encryptions_sec=AES_kilobytes*1024*8/AES_blockwidth
+ # AES_encryptions_year=AES_encryptions_sec*60*60*24*365
+ # AES_years=sec_level-log2(AES_encryptions_year)
+ AES_years = AES_GATE_COUNT_LEVEL[sec_level]
+ # McEliece_1284_years=16.01/365
+
+ if mem_indicator == 0:
+ max_mem = inf
+ elif mem_indicator == 1:
+ max_mem = 80-log2(n)
+ else:
+ max_mem = 60-log2(n)
+
+ # McEliece_1284_complexity=bjmm_depth_2_qc_complexity(1284,1027,24,memory_access=access_cost,enable_tmto=1)["time"]+log2(1284)
+ McEliece_big_complexity = bjmm_depth_2_qc_complexity(
+ n, k, w, memory_access=access_cost, mem=max_mem, enable_tmto=tm)["time"]+log2(n)
+ # McEliece_big_years=log2(McEliece_1284_years)+McEliece_big_complexity-McEliece_1284_complexity
+
+ McEliece_big_years = McEliece_big_complexity
+
+ if verbose:
+ if AES_years-McEliece_big_years < 0:
+ print("McEliece is", abs(
+ AES_years-McEliece_big_years), "bits harder")
+ else:
+ print("AES is", abs(
+ AES_years-McEliece_big_years), "bits harder")
+ levels_mceliece[access_cost][mem_indicator].append(
+ McEliece_big_years-AES_years)
+
+
+def compute_qc_table_vs_aes(verbose=True):
+ verbose = 1
+ levels_qc = {"BIKEmsg": [[], [], []],
+ "BIKEkey": [[], [], []], "HQC": [[], [], []]}
+ for bike_o_hqc in ["HQC", "BIKEmsg", "BIKEkey",]:
+ for access_cost in range(3):
+ for mem_indicator in range(1):
+ levels_qc[bike_o_hqc][access_cost].append([])
+ for sec_level in [128, 192, 256]:
+ if sec_level == 128:
+ c = params_qc[bike_o_hqc][0]
+ elif sec_level == 192:
+ c = params_qc[bike_o_hqc][1]
+ else:
+ c = params_qc[bike_o_hqc][2]
+
+ n = c["n"]
+ k = c["k"]
+
+ if bike_o_hqc == "BIKEkey":
+ w = c["w_k"]
+ else:
+ w = c["w"]
+
+ print(n, k, w)
+ # AES_kilobytes*=2
+ # AES_blockwidth=128 if sec_level==128 else 256
+ # AES_kilobytes=AES[str(AES_blockwidth)][str(sec_level)]
+ # AES_encryptions_sec=AES_kilobytes*1024*8/AES_blockwidth
+ # AES_encryptions_year=AES_encryptions_sec*60*60*24*365
+ # AES_years=sec_level-log2(AES_encryptions_year)
+ AES_years = AES_GATE_COUNT_LEVEL[sec_level]
+
+ if mem_indicator == 0:
+ max_mem = inf
+ elif mem_indicator == 1:
+ max_mem = 60-log2(n)
+ else:
+ max_mem == 80-log2(n)
+
+ # Experiment_years=38.16/24/365
+ # Experiment_complexity=bjmm_depth_2_qc_complexity(3138,3138//2,56,qc=1,memory_access=access_cost,enable_tmto=1)["time"]+log2(3138)
+ qc = 0 if bike_o_hqc == "BIKEkey" else 1
+
+ big_complexity = bjmm_depth_2_qc_complexity(
+ n, k, w, memory_access=access_cost, mem=max_mem, qc=qc, enable_tmto=1)["time"]+log2(n)
+ # McEliece_big_complexity=bjmm_depth_2_qc_complexity(n,k,w,memory_access=access_cost,mem=max_mem,enable_tmto=1)["time"]+log2(n)
+
+ if bike_o_hqc == "BIKEkey":
+ big_complexity -= log2(k)
+
+ # big_years = log2(Experiment_years)+big_complexity-Experiment_complexity
+ big_years = big_complexity
+ #print(big_complexity, AES_years)
+ if verbose:
+ if AES_years-big_years < 0:
+ name = bike_o_hqc
+ print(name, "is", abs(
+ AES_years-big_years), "bits harder")
+ else:
+ print("AES is", abs(AES_years-big_years), "bits harder")
+
+ levels_qc[bike_o_hqc][access_cost][mem_indicator].append(
+ big_years-AES_years)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description='SSS and decoding optimizer')
+ parser.add_argument('--bcj', action='store_true',
+ help='optimize the BCJ algorithm')
+ parser.add_argument('--new_bcj', action='store_true',
+ help='optimize the BCJ algorithm')
+ parser.add_argument('--bbss', action='store_true',
+ help='optimize the BBSS algorithm')
+ parser.add_argument('--new_bbss', action='store_true',
+ help='optimize the BBSS algorithm')
+
+ parser.add_argument('--bjmm', action='store_true',
+ help='optimize the BJMM algorithm')
+ parser.add_argument('--new_bjmm', action='store_true',
+ help='optimize the new BJMM algorithm')
+ parser.add_argument('--mmt', action='store_true',
+ help='optimize the MMT algorithm')
+ parser.add_argument('--new_mmt', action='store_true',
+ help='optimize the new MMT algorithm')
+
+ parser.add_argument('-k', type=float, default=0.488,
+ help='code dimension, only for bjmm/mmt')
+ parser.add_argument('-w', type=float, default=Hi(1-0.488)/2,
+ help='optimize the new MMT algorithm')
+
+ parser.add_argument('--verbose', action='store_true',
+ help='verbose output')
+ parser.add_argument('--retries', type=int, default=10000,
+ help='number of retries per optimisation step')
+ parser.add_argument('--memlimit', action='store_true',
+ help='optimize under memory limitation')
+ parser.add_argument('--rate', action='store_true',
+ help='optimize under memory limitation')
+
+ args = parser.parse_args()
+
+ if args.rate:
+ if args.mmt:
+ optimize_k_mmt()
+ if args.new_mmt:
+ optimize_k_new_mmt()
+ if args.bjmm:
+ optimize_k_bjmm()
+ if args.new_bjmm:
+ optimize_k_new_bjmm()
+
+ if args.memlimit:
+ # global NOLOG
+ NOLOG = True
+
+ if args.mmt:
+ optimize_mem_mmt()
+ if args.new_mmt:
+ optimize_mem_new_mmt()
+ if args.bjmm:
+ optimize_mem_bjmm()
+ if args.new_bjmm:
+ optimize_mem_new_bjmm()
+
+ bbss = False
+ if args.bbss or args.new_bbss:
+ bbss = True
+
+ new = False
+ if args.new_bbss or args.new_bcj:
+ new = True
+
+ BCJ_BBSS_memlimit(bbss, new)
+ exit(0)
+
+ memlimit = 1.
+ if args.bcj:
+ optimize_bcj(False, verb=args.verbose, iters=args.retries, membound=memlimit)
+ if args.new_bcj:
+ optimize_bcj(True, verb=args.verbose, iters=args.retries, membound=memlimit)
+ if args.bbss:
+ optimize_bbss(False, verb=args.verbose, iters=args.retries, membound=memlimit)
+ if args.new_bbss:
+ optimize_bbss(True, verb=args.verbose, iters=args.retries, membound=memlimit)
+ if args.bjmm:
+ optimize_bjmm(k=args.k, w=args.w, verb=args.verbose, iters=args.retries, membound=memlimit)
+ if args.new_bjmm:
+ optimize_new_bjmm(k=args.k, w=args.w, verb=args.verbose, iters=args.retries, membound=memlimit)
+ if args.mmt:
+ optimize_mmt(k=args.k, w=args.w, verb=args.verbose, iters=args.retries, membound=memlimit)
+ if args.new_mmt:
+ optimize_new_mmt(k=args.k, w=args.w, verb=args.verbose, iters=args.retries, membound=memlimit)
diff --git a/tests/module/our_cost.sage b/tests/module/our_cost.sage
new file mode 100644
index 00000000..fd51674c
--- /dev/null
+++ b/tests/module/our_cost.sage
@@ -0,0 +1,90 @@
+load('tests/module/cost.sage');
+
+###################################################
+
+def gauss_binomial(m,r,q):
+
+ x = 1.;
+ for i in range(r):
+ x = x*(1.-q^(m-i*1.))/(1.-q^(1.+i));
+
+ return x;
+
+##################################
+#Compute running time of our algorithm
+
+def compute_new_cost(n,m,q,ell):
+
+
+
+ best_cost = 10000000000000000; #optimal cost (i.e., minimum running time)
+
+ #Optimal parameters
+ best_d = 0;
+ best_w = 0;
+ best_w1 = 0;
+ best_u = 0;
+ best_isd = 0;
+
+ for d in range(1,m+1):
+
+ for w in range(1,n):
+
+
+ N_w = binomial(n,w)*(q^d-1)^(w-d)*gauss_binomial(m,d,q)/gauss_binomial(n,d,q); #number of desired subcodes
+
+
+ if N_w>1: #continue only if, on average, at least one subcode exists
+
+
+ c_isd = cost_isd(q,n,m,d,w,N_w);
+
+ for w1 in range(1,w):
+
+ w2 = w-w1;
+
+ T_K = factorial(n)/factorial(n-w1)+factorial(n)/factorial(n-w2)+factorial(n)^2*q^(-d*ell)/(factorial(n-w1)*factorial(n-w2));
+
+ if log2(T_K) Nw_prime - 1:
+ L_prime= 2^L_prime
+ Nw_prime=2^Nw_prime
+ # ____________________Approximation_________________ _________________Coupon Collector___________________________
+ return (log2(L_prime)-log2(Nw_prime)+log2(log2(L_prime))) - log2(2*log(1.-L_prime/Nw_prime)/log(1.-1/Nw_prime)/Nw_prime)
+ return 0
+
+
+def test_bbps2():
+ """
+ generic test
+ """
+ ranges = 0.2
+
+
+ for n in range(100, 120,5):
+ for k in range(n//2,n//2+10,2):
+ for q in [7, 11, 17, 31]:
+ A = BBPS(LEProblem(n, k, q), **bbps_params)
+ t1 = A.time_complexity()
+ t2, w, w_prime, L_prime = improved_linear_beullens(n, k, q)
+
+ if t2 == 100000000000000:
+ continue
+
+ verb = A._get_verbose_information()
+ L,N= verb["L_prime"],verb["Nw_prime"]
+
+ t2 += correct_coupon_collector(L,N)
+ assert t2 - ranges < t1 < t2 + ranges
+
+
+if __name__ == "__main__":
+ test_bbps1()
+ test_bbps2()
diff --git a/tests/test_le_beullens.sage b/tests/test_le_beullens.sage
new file mode 100644
index 00000000..3b36ae29
--- /dev/null
+++ b/tests/test_le_beullens.sage
@@ -0,0 +1,52 @@
+import random
+from cryptographic_estimators.LEEstimator.LEAlgorithms import *
+from cryptographic_estimators.LEEstimator import LEProblem
+from cryptographic_estimators.SDFqEstimator.SDFqAlgorithms import LeeBrickell, Prange, Stern
+from math import inf
+
+load('tests/module/attack_cost.sage')
+beullens_params = {"bit_complexities": 0}
+
+ranges = 0.1
+
+
+def test_beullens1():
+ """
+ special value test
+ """
+ n = 250
+ k = 125
+ for q in [11, 17, 31, 53]:
+ A = Beullens(LEProblem(n, k, q), **beullens_params)
+ t1=A.time_complexity()
+ t2 = attack_cost(n, k, q) + log(n,2)
+ assert t2 - ranges < t1 < t2 + ranges
+
+
+
+def test_beullens2():
+ """
+ small `n` test.
+ """
+ for n in range(100, 103):
+ for k in range(50, 53):
+ for q in [7, 11]:
+ A = Beullens(LEProblem(n, k, q), **beullens_params)
+ B = Beullens(LEProblem(n, k, q), **beullens_params)
+ t1 = min(A.time_complexity(),B.time_complexity())
+ t21 = attack_cost(n, k, q)
+ t22 = attack_cost(n, k, q)
+
+ if t21 is None and t22 is None:
+ continue
+ t2 = min(t21, t22)
+ if t1 == inf or t2 == inf:
+ continue
+
+ t2 += log(n,2)
+ assert t2 - ranges < t1 < t2 + ranges
+
+
+if __name__ == "__main__":
+ test_beullens1()
+ test_beullens2()
\ No newline at end of file
diff --git a/tests/test_mq.py b/tests/test_mq.py
new file mode 100644
index 00000000..b7e7e7fe
--- /dev/null
+++ b/tests/test_mq.py
@@ -0,0 +1,96 @@
+
+from sage.all_cmdline import *
+
+from math import log2
+from .module.multivariate_quadratic_estimator.mpkc.algorithms.bjorklund import Bjorklund as TestBjorklund
+from .module.multivariate_quadratic_estimator.mpkc.algorithms.boolean_solve_fxl import BooleanSolveFXL as TestBooleanSolveFXL
+from .module.multivariate_quadratic_estimator.mpkc.algorithms.cgmta import CGMTA as TestCGMTA
+from .module.multivariate_quadratic_estimator.mpkc.algorithms.crossbred import Crossbred as TestCrossbred
+from .module.multivariate_quadratic_estimator.mpkc.algorithms.dinur1 import DinurFirst as TestDinurFirst
+from .module.multivariate_quadratic_estimator.mpkc.algorithms.dinur2 import DinurSecond as TestDinurSecond
+from .module.multivariate_quadratic_estimator.mpkc.algorithms.exhaustive_search import ExhaustiveSearch as TestExhaustiveSearch
+from .module.multivariate_quadratic_estimator.mpkc.algorithms.f5 import F5 as TestF5
+from .module.multivariate_quadratic_estimator.mpkc.algorithms.hybrid_f5 import HybridF5 as TestHybridF5
+from .module.multivariate_quadratic_estimator.mpkc.algorithms.kpg import KPG as TestKPG
+from .module.multivariate_quadratic_estimator.mpkc.algorithms.lokshtanov import Lokshtanov as TestLokshtanov
+from .module.multivariate_quadratic_estimator.mpkc.algorithms.mht import MHT as TestMHT
+from cryptographic_estimators.MQEstimator.MQAlgorithms import Bjorklund, BooleanSolveFXL, CGMTA, Crossbred, \
+ DinurFirst, DinurSecond, ExhaustiveSearch, F5, HybridF5, KPG, Lokshtanov, MHT
+
+from cryptographic_estimators.MQEstimator import MQProblem
+
+ranges = 0.01
+
+test_sets = [
+ # n m q
+ [50, 50, 2],
+ [70, 70, 4],
+ [50, 70, 8],
+ [120, 40, 8],
+
+]
+
+algos = [
+ Bjorklund,
+ BooleanSolveFXL,
+ CGMTA,
+ Crossbred,
+ DinurFirst,
+ DinurSecond,
+ ExhaustiveSearch,
+ F5,
+ HybridF5,
+ KPG,
+ Lokshtanov,
+ MHT
+]
+
+test_algos = [
+ TestBjorklund,
+ TestBooleanSolveFXL,
+ TestCGMTA,
+ TestCrossbred,
+ TestDinurFirst,
+ TestDinurSecond,
+ TestExhaustiveSearch,
+ TestF5,
+ TestHybridF5,
+ TestKPG,
+ TestLokshtanov,
+ TestMHT
+]
+
+
+def test_all():
+ """
+ tests that all estimations match those from https://github.com/Crypto-TII/multivariate_quadratic_estimatorup to
+ a tolerance of 0.01 bit
+ """
+ assert len(algos) == len(test_algos)
+ for i, _ in enumerate(test_algos):
+ A1 = algos[i]
+ A2 = test_algos[i]
+ for set in test_sets:
+ n, m, q = set[0], set[1], set[2]
+ print(n, m, q)
+
+ try:
+ Alg1 = A1(MQProblem(n=n, m=m, q=q), bit_complexities=0)
+ if q == 2 and A1 in [Bjorklund, DinurFirst, DinurSecond]:
+ Alg2 = A2(n=n, m=m)
+ else:
+ Alg2 = A2(n=n, m=m, q=q)
+
+ except:
+ continue
+
+ T1 = Alg1.time_complexity()
+ T2 = log2(Alg2.time_complexity())
+ print(Alg1._name, T1, T2)
+ # print(Alg1.optimal_parameters())
+ # print(Alg2._optimal_parameters)
+ assert T2 - ranges <= T1 <= T2 + ranges
+
+
+if __name__ == "__main__":
+ test_all()
diff --git a/tests/test_pe.sage b/tests/test_pe.sage
new file mode 100644
index 00000000..d98b3908
--- /dev/null
+++ b/tests/test_pe.sage
@@ -0,0 +1,111 @@
+from cryptographic_estimators.SDFqEstimator.SDFqAlgorithms import LeeBrickell, Prange, Stern
+from cryptographic_estimators.PEEstimator.PEAlgorithms import Leon, Beullens
+from cryptographic_estimators.PEEstimator import PEProblem
+from cryptographic_estimators.PEEstimator.pe_helper import number_of_weight_d_codewords, gv_distance
+
+
+load('tests/module/attack_cost.sage')
+load('tests/module/cost.sage')
+
+# global parameters
+leon_params = {"bit_complexities": 0, "sd_parameters": {"excluded_algorithms": [Prange, Stern]}}
+
+# correction term due to correction of the LeeBrickell procedure, see SDFqAlgorithms/leebrickell.py line 98/99
+def lee_brickell_correction(k):
+ return log(k, 2)*2 - log(binomial(k, 2), 2)
+
+
+def test_leon1():
+ """
+ test some hardcoded values taken from:
+ https://github.com/WardBeullens/LESS_Attack/blob/master/attack_cost.py
+ """
+
+ n, k, q = 250, 125, 53
+ ranges = 0.01
+ t1 = LEON(n, k, q) + log(n, 2) - lee_brickell_correction(k)
+
+ A = Leon(PEProblem(n, k, q), **leon_params)
+ t2 = A.time_complexity()
+ assert t1 - ranges <= t2 <= t1 + ranges
+
+ n, k, q = 106, 45, 7
+ t1 = LEON(n, k, q) + log(n, 2) - lee_brickell_correction(k)
+ t2 = Leon(PEProblem(n, k, q), **leon_params).time_complexity()
+ assert t1 - ranges <= t2 <= t1 + ranges
+
+
+def test_leon2():
+ """
+ test some hardcoded values from:
+ https://github.com/WardBeullens/LESS_Attack/blob/master/attack_cost.py
+ """
+ ranges = 0.01
+ n, k= 250, 150
+ q_values = [11, 17, 53, 103, 151, 199, 251]
+ for q in q_values:
+ t1 = LEON(n, k, q) + log2(n) - lee_brickell_correction(k)
+ t2 = Leon(PEProblem(n, k, q), **leon_params).time_complexity()
+ print(n,k,q,t1,t2)
+ assert t1 - ranges <= t2 <= t1 + ranges
+
+
+def test_leon():
+ """
+ tests leon on small instances
+ """
+ # for small values due to rounding issues we have to slightly increase the tolerance
+ ranges = 0.2
+ for n in range(50, 100, 5):
+ for k in range(n//2, n//2+5):
+ for q in [3, 7, 17, 31]:
+ A = Leon(PEProblem(n, k, q), **leon_params)
+
+ # due to slightly different calculation of "number_of_weight_d_codewords" optimal w might differ by 1
+ # for some edge cases
+ t11 = A.time_complexity()
+ t12 = A.time_complexity(w=A.optimal_parameters()["w"]+1)
+
+ t2 = LEON(n, k, q) + log2(n) - lee_brickell_correction(k)
+ assert t2 - ranges < t11 < t2 + ranges or t2 - ranges < t12 < t2 + ranges
+
+
+def test_beullens():
+ """
+ test some hardcoded values taken from:
+ https://github.com/WardBeullens/LESS_Attack/blob/master/attack_cost.py
+ """
+
+ n, k, q = 250, 125, 53
+ ranges = 0.01
+ ts = []
+ ts2 = []
+ for i in range(10):
+ ts.append(attack_cost(n, k, q, False, False) + log(n, 2) - lee_brickell_correction(k))
+ A = Beullens(PEProblem(n, k, q), **leon_params)
+ ts2.append(A.time_complexity())
+ t1 = min(ts)
+ t2 = min(ts2)
+ print(t2, t1, A.optimal_parameters())
+ assert t1 - ranges <= t2 <= t1 + ranges
+
+
+ n, k, q = 106, 45, 7
+ ts = []
+ ts2 = []
+ for i in range(10):
+ ts.append(attack_cost(n, k, q, False, False) + log(n, 2) - lee_brickell_correction(k))
+ A = Beullens(PEProblem(n, k, q), **leon_params)
+ ts2.append(A.time_complexity())
+ t1 = min(ts)
+ t2 = min(ts2)
+ print(t2, t1, A.optimal_parameters())
+ assert t1 - ranges <= t2 <= t1 + ranges
+
+
+
+if __name__ == "__main__":
+ test_beullens()
+ test_leon1()
+ test_leon2()
+ test_leon()
diff --git a/tests/test_pk.sage b/tests/test_pk.sage
new file mode 100644
index 00000000..53820565
--- /dev/null
+++ b/tests/test_pk.sage
@@ -0,0 +1,84 @@
+from cryptographic_estimators.PKEstimator import PKProblem
+from cryptographic_estimators.PKEstimator.PKAlgorithms import KMP, SBC
+from cryptographic_estimators.SDFqEstimator.SDFqAlgorithms import Prange, LeeBrickell
+load('tests/module/cost.sage')
+load('tests/module/kmp_cost.sage')
+load('tests/module/our_cost.sage')
+
+
+# global parameters
+params = {"bit_complexities": False, "cost_for_list_operation": 1, "memory_for_list_element": 1,
+ "sd_parameters": {"excluded_algorithms": [Prange, LeeBrickell]}}
+ranges = 0.1
+
+
+def test_kmp():
+ """
+ special value test
+ """
+ n = 94
+ m = 55
+ q = 509
+ ell = 1
+
+ t1 = KMP(PKProblem(n, m, q, ell), **params).time_complexity()
+ _, _, _, _, _, t2 = kmp_cost_numerical(n, m, ell, q)
+ assert t1 - ranges <= t2 <= t1 + ranges
+
+def test_sbc():
+ """
+ special value test
+ """
+ n = 94
+ m = 55
+ q = 509
+ ell = 1
+
+ A = SBC(PKProblem(n, m, q, ell), **params)
+ t1 = A.time_complexity()
+ _, _, _, _, _, t2 = compute_new_cost(n, m, q, ell)
+ assert t1 - ranges <= t2 <= t1 + ranges
+
+
+def test_kmp_range():
+ """
+ small value test
+ """
+ # we adapted the KMP algorithm to allow to enumerate on m.
+ for n in range(30, 100):
+ for m in range(int(0.3 * n), int(0.7 * n)):
+ for ell in range(1, 2):
+ for q in [7, 11, 17, 53, 103, 151, 199, 251]:
+ if q^ell < n:
+ continue
+ A = KMP(PKProblem(n, m, q, ell), **params)
+ t1 = A.time_complexity()
+ _, _, _, _, _, t2 = kmp_cost_numerical(n, m, ell, q)
+
+ assert t2 - ranges < t1 < t2 + ranges
+
+
+def test_sbc_range():
+ """
+ small value test
+ """
+
+ for n in range(30, 50,5):
+ for m in range(n//2, n//2 + 5):
+ for ell in range(1,3):
+ for q in [ 53, 151, 251]:
+ if q^ell < n:
+ continue
+
+ t1 = SBC(PKProblem(n, m, q, ell), **params).time_complexity()
+ _, _, _, _, _, t2 = compute_new_cost(n, m, q, ell)
+
+ print(n, m, q, ell, t1, t2)
+ assert t2 - ranges < t1 < t2 + ranges
+
+
+if __name__ == "__main__":
+ test_kmp()
+ test_sbc()
+ test_kmp_range()
+ test_sbc_range()
diff --git a/tests/test_sd.py b/tests/test_sd.py
new file mode 100644
index 00000000..1ce66e02
--- /dev/null
+++ b/tests/test_sd.py
@@ -0,0 +1,95 @@
+from cryptographic_estimators.SDEstimator.SDAlgorithms import Prange, Dumer, BallCollision, BJMMd2, BJMMd3, BJMM, \
+ BJMMdw, \
+ BJMMpdw, BothMay, MayOzerov, MayOzerovD2, MayOzerovD3, Stern, BJMM_plus
+from cryptographic_estimators.SDEstimator import SDProblem
+from .module.estimator import prange_complexity, dumer_complexity, stern_complexity, ball_collision_decoding_complexity, \
+ bjmm_depth_2_complexity, bjmm_depth_3_complexity, bjmm_complexity, bjmm_depth_2_disjoint_weight_complexity, \
+ bjmm_depth_2_partially_disjoint_weight_complexity, both_may_depth_2_complexity, may_ozerov_complexity, \
+ may_ozerov_depth_2_complexity, may_ozerov_depth_3_complexity
+from .module.optimize import bjmm_depth_2_qc_complexity
+
+ranges = 0.01
+
+test_sets = [
+ [100, 50, 10],
+ [1284, 1028, 24],
+ [3488, 2720, 64],
+]
+
+algos = [
+ Prange,
+ Stern,
+ Dumer,
+ BallCollision,
+ BJMMd2,
+ BJMMd3,
+ BJMM,
+ BJMMdw,
+ BJMMpdw,
+ BothMay,
+ MayOzerov,
+ MayOzerovD2,
+ MayOzerovD3,
+]
+
+test_algos = [
+ prange_complexity,
+ stern_complexity,
+ dumer_complexity,
+ ball_collision_decoding_complexity,
+ bjmm_depth_2_complexity,
+ bjmm_depth_3_complexity,
+ bjmm_complexity,
+ bjmm_depth_2_disjoint_weight_complexity,
+ bjmm_depth_2_partially_disjoint_weight_complexity,
+ both_may_depth_2_complexity,
+ may_ozerov_complexity,
+ may_ozerov_depth_2_complexity,
+ may_ozerov_depth_3_complexity,
+]
+
+
+def test_all():
+ """
+ tests that all estimations match those from https://github.com/Crypto-TII/syndrome_decoding_estimator up to
+ a tolerance of 0.01 bit
+ """
+ assert len(algos) == len(test_algos)
+ for i, _ in enumerate(test_algos):
+ A1 = algos[i]
+ A2 = test_algos[i]
+ for set in test_sets:
+ n, k, w = set[0], set[1], set[2]
+ Alg = A1(SDProblem(n=n, k=k, w=w), bit_complexities=0)
+ Alg2 = A2(n=n, k=k, w=w)
+
+ # Slight correction of parameter ranges leads to (slightly) better parameters in case of the
+ # CryptographicEstimators for Both-May and May-Ozerov. For test we fix parameters to the once from the
+ # online code.
+ if Alg._name == "Both-May" or Alg._name == "May-OzerovD2" or Alg._name == "May-OzerovD3":
+ too_much = [i for i in Alg2["parameters"] if i not in Alg.parameter_names()]
+ for i in too_much:
+ Alg2["parameters"].pop(i)
+ Alg.set_parameters(Alg2["parameters"])
+
+ T1 = Alg.time_complexity()
+ T2 = Alg2["time"]
+ assert T2 - ranges <= T1 <= T2 + ranges
+
+
+def test_bjmm_plus():
+ """
+ tests that BJMM+ estimation matches the one from https://github.com/FloydZ/Improving-ISD-in-Theory-and-Practice
+ up to a tolerance of 0.01 bit
+ """
+ for set in test_sets:
+ n, k, w = set[0], set[1], set[2]
+ t = bjmm_depth_2_qc_complexity(n, k, w)
+ t1 = t["time"]
+ t2 = BJMM_plus(SDProblem(n, k, w), bit_complexities=0).time_complexity()
+ assert t1 - ranges <= t2 <= t1 + ranges
+
+
+if __name__ == "__main__":
+ test_all()
+ test_bjmm_plus()
diff --git a/tests/test_sdfq.sage b/tests/test_sdfq.sage
new file mode 100644
index 00000000..fcffad8a
--- /dev/null
+++ b/tests/test_sdfq.sage
@@ -0,0 +1,80 @@
+from cryptographic_estimators.SDFqEstimator import SDFqEstimator
+from cryptographic_estimators.SDFqEstimator.SDFqAlgorithms import *
+from cryptographic_estimators.SDFqEstimator import SDFqProblem
+from math import floor, log2, ceil, comb, comb as binomial, log2 as log
+
+
+load('tests/module/attack_cost.sage')
+load('tests/module/cost.sage')
+
+
+# global parameters
+params = {"bit_complexities": 0, "is_syndrome_zero": True, "nsolutions": 0}
+stern_params = {"bit_complexities": 1, "is_syndrome_zero": True, "nsolutions": 0}
+
+# correction term due to correction of the LeeBrickell procedure, see SDFqAlgorithms/leebrickell.py line 98/99
+def lee_brickell_correction(k):
+ return log(k, 2)*2 - log(binomial(k, 2), 2)
+
+
+def test_sdfq_LeeBrickell():
+ """
+ special value test for Lee-Brickell
+ """
+ # we need to subtract the difference of lee brickell
+ # and p = 1, because p = 2 is not always optimial
+ ranges = 0.01
+ n, k, w, q = 256, 128, 64, 251
+ t1 = log(ISD_COST(n, k, w, q), 2) + log(n,2) - lee_brickell_correction(k)
+ A = LeeBrickell(SDFqProblem(n, k, w, q, **params), **params)
+ t2 = A.time_complexity(p=2)
+ assert(t1 - ranges < t2 < t1 + ranges)
+
+ n, k, w, q = 961, 771, 48, 31
+ t1 = log(ISD_COST(n, k, w, q), 2) + log(n,2) - lee_brickell_correction(k)
+ A = LeeBrickell(SDFqProblem(n, k, w, q, **params), **params)
+ t2 = A.time_complexity(**{"p": 2})
+ assert(t1 - ranges < t2 < t1 + ranges)
+
+
+def test_sdfq_stern():
+ """
+ special value test for Stern
+ """
+ ranges = 0.01
+ n, k, w, q = 256, 128, 64, 251
+ t, p, l = peters_isd(n, k, q, w)
+ t1 = Stern(SDFqProblem(n, k, w, q, **stern_params), **stern_params).time_complexity()
+ assert(t - ranges < t1 < t + ranges)
+
+ n, k, w, q = 961, 771, 48,31
+ t, p, l = peters_isd(n, k, q, w)
+
+ assert(t - ranges < Stern(SDFqProblem(n, k, w, q, **stern_params), **stern_params).time_complexity() < t + ranges)
+
+
+def test_sdfq_stern_range():
+ """
+ range test for Stern
+ """
+ ranges = 0.05
+
+ for n in range(50, 70, 5):
+ for k in range(20, 40, 2):
+ for w in range(4, min(n - k - 1, int(0.5 * n))):
+ for q in [7, 11, 17, 53, 103, 151, 199, 251]:
+ A=Stern(SDFqProblem(n, k, w, q, **stern_params), **stern_params)
+ # peters_isd comparison code restricts l to this range
+ A.set_parameter_ranges("l", 1, n-k)
+ t1 = A.time_complexity()
+ print(A.optimal_parameters())
+ t2, p, l = peters_isd(n, k, q, w)
+
+ print(n,k,q,w,t1,t2)
+ assert t2 - ranges < t1 < t2 + ranges
+
+
+if __name__ == "__main__":
+ test_sdfq_LeeBrickell()
+ test_sdfq_stern()
+ test_sdfq_stern_range()
\ No newline at end of file