diff --git a/CITATION.cff b/CITATION.cff index 8982711590a..043ee1a53ef 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,11 +1,9 @@ cff-version: 1.2.0 message: "If you use this software, please cite it as below." -title: SageMath -abstract: SageMath is a free open-source mathematics software system. +title: SageMath with flag algebras +abstract: SageMath is a free open-source mathematics software system, this fork in addition contains the implementation of flag algbras. authors: - name: "The SageMath Developers" +- name: "Levente Bodnar" version: 10.5.beta7 -doi: 10.5281/zenodo.8042260 -date-released: 2024-10-12 -repository-code: "https://github.com/sagemath/sage" -url: "https://www.sagemath.org/" +repository-code: "https://github.com/bodnalev/sage" diff --git a/CITATION.cff.in b/CITATION.cff.in index a3d4e1a35bd..ef46e6c151d 100644 --- a/CITATION.cff.in +++ b/CITATION.cff.in @@ -1,11 +1,9 @@ cff-version: 1.2.0 message: "If you use this software, please cite it as below." -title: SageMath -abstract: SageMath is a free open-source mathematics software system. +title: SageMath with flag algebras +abstract: SageMath is a free open-source mathematics software system, this fork in addition contains the implementation of flag algbras. authors: - name: "The SageMath Developers" +- name: "Levente Bodnar" version: ${SAGE_VERSION} -doi: 10.5281/zenodo.8042260 -date-released: ${SAGE_RELEASE_DATE} -repository-code: "https://github.com/sagemath/sage" -url: "https://www.sagemath.org/" +repository-code: "https://github.com/bodnalev/sage" diff --git a/README.md b/README.md index af91374fe19..eb35f5a8186 100644 --- a/README.md +++ b/README.md @@ -14,408 +14,52 @@ Sage is open source mathematical software released under the GNU General Public Licence GPLv2+, and includes packages that have [compatible software licenses](./COPYING.txt). [People all around the globe](https://www.sagemath.org/development-map.html) have contributed to the -development of Sage. [Full documentation](https://doc.sagemath.org/html/en/index.html) is available online. +development of Sage. -Table of Contents +Flag algebras ----------------- -* [Getting Started](#getting-started) -* [Supported Platforms](#supported-platforms) -* [\[Windows\] Preparing the Platform](#windows-preparing-the-platform) -* [\[macOS\] Preparing the Platform](#macos-preparing-the-platform) -* [Instructions to Build from Source](#instructions-to-build-from-source) -* [SageMath Docker Images](#sagemath-docker-images) -* [Troubleshooting](#troubleshooting) -* [Contributing to Sage](#contributing-to-sage) -* [Directory Layout](#directory-layout) -* [Build System](#build-system) -* [Relocation](#relocation) -* [Redistribution](#redistribution) -* [Build System](#build-system) -* [Changes to Included Software](#changes-to-included-software) - -Getting Started ---------------- - -Those who are impatient may use prebuilt Sage available online from any of - -[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/sagemath/sage-binder-env/master -)   [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/sagemath/sage/tree/master -)   [![Open in GitHub Codespaces](https://img.shields.io/badge/Open_in_GitHub_Codespaces-black?logo=github)](https://codespaces.new/sagemath/sage/tree/master) - -without local installation. Otherwise read on. - -The [Sage Installation Guide](https://doc.sagemath.org/html/en/installation/index.html) -provides a decision tree that guides you to the type of installation -that will work best for you. This includes building from source, -obtaining Sage from a package manager, using a container image, or using -Sage in the cloud. - -**This README contains self-contained instructions for building Sage from source.** -This requires you to clone the git repository (as described in this README) or download the -[sources](https://www.sagemath.org/download-source.html) in the form -of a tarball. - -If you have questions or encounter problems, please do not hesitate -to email the [sage-support mailing list](https://groups.google.com/group/sage-support) -or ask on the [Ask Sage questions and answers site](https://ask.sagemath.org). - -Supported Platforms -------------------- - -Sage attempts to support all major Linux distributions, recent versions of -macOS, and Windows (using Windows Subsystem for Linux or -virtualization). - -Detailed information on supported platforms for a specific version of Sage -can be found in the section _Availability and installation help_ of the -[release tour for this version](https://github.com/sagemath/sage/releases). - -We highly appreciate contributions to Sage that fix portability bugs -and help port Sage to new platforms; let us know at the [sage-devel -mailing list](https://groups.google.com/group/sage-devel). - -[Windows] Preparing the Platform --------------------------------- - -The preferred way to run Sage on Windows is using Windows Subsystem for -Linux (WSL). Follow the -[official WSL setup guide](https://docs.microsoft.com/en-us/windows/wsl/faq) -to install Ubuntu (or another Linux distribution). -Make sure you allocate WSL sufficient RAM; 5GB is known to work, while -2GB might be not enough for building Sage from source. -Then all instructions for installation in Linux apply. - -As an alternative, you can also run Linux on Windows using Docker ([see -below](#sagemath-docker-images)) or other virtualization solutions. - -[macOS] Preparing the Platform ------------------------------- - -- If your Mac uses the Apple Silicon (M1, M2, M3, M4; arm64) architecture and - you set up your Mac by transferring files from an older Mac, make sure - that the directory ``/usr/local`` does not contain an old copy of Homebrew - (or other software) for the x86_64 architecture that you may have copied - over. Note that Homebrew for the M1 is installed in ``/opt/homebrew``, not - ``/usr/local``. - -- If you wish to use conda, please see the [section on - conda](https://doc.sagemath.org/html/en/installation/conda.html) in the Sage - Installation Manual for guidance. - -- Otherwise, we strongly recommend to use Homebrew ("the missing package - manager for macOS") from https://brew.sh/, which provides the ``gfortran`` - compiler and many libraries. - -- Otherwise, if you do not wish to install Homebrew, you will need to install - the latest version of Xcode Command Line Tools. Open a terminal window and - run `xcode-select --install`; then click "Install" in the pop-up window. If - the Xcode Command Line Tools are already installed, you may want to check if - they need to be updated by typing `softwareupdate -l`. +This repository is a copy of the official SageMath project with additional functionality to handle +flag algebraic calculations. Explore the capabilities and functionalities of the package related to flag algebras by visiting the [tutorial notebook](https://github.com/bodnalev/sage/blob/flag-algebras/flag_tutorial.ipynb). Instructions to Build from Source --------------------------------- -Like many other software packages, Sage is built from source using -`./configure`, followed by `make`. However, we strongly recommend to -read the following step-by-step instructions for building Sage. - -The instructions cover all of Linux, macOS, and WSL. - -More details, providing a background for these instructions, can be found -in the section [Install from Source Code](https://doc.sagemath.org/html/en/installation/source.html) -in the Installation Guide. - - -1. Decide on the source/build directory (`SAGE_ROOT`): - - - On personal computers, any subdirectory of your :envvar:`HOME` - directory should do. - - - For example, you could use `SAGE_ROOT=~/sage/sage`, which we - will use as the running example below. - - - You need at least 10 GB of free disk space. - - - The full path to the source directory must contain **no spaces**. +Sage attempts to support all major Linux distributions, recent versions of +macOS, and Windows (using Windows Subsystem for Linux or +virtualization). The additional software and packages needed for flag algebraic calculations +are only tested on a few Debian based distributions and on Mac. This guide is only guaranteed to work on Debian based distributions. +1. Prepare the environment: + - At least 10 GB of free space is required. + - Pick a path for the folder that will contain the source files. Note it **can not contain spaces**. - After starting the build, you cannot move the source/build directory without breaking things. - - - You may want to avoid slow filesystems such as - [network file systems (NFS)](https://en.wikipedia.org/wiki/Network_File_System) - and the like. - - - [macOS] macOS allows changing directories without using exact capitalization. - Beware of this convenience when compiling for macOS. Ignoring exact - capitalization when changing into :envvar:`SAGE_ROOT` can lead to build - errors for dependencies requiring exact capitalization in path names. - -2. Clone the sources with `git`: - - - To check that `git` is available, open a terminal and enter - the following command at the shell prompt (`$`): - - $ git --version - git version 2.42.0 - - The exact version does not matter, but if this command gives an error, - install `git` using your package manager, using one of these commands: - - $ sudo pacman -S git # on Arch Linux - $ sudo apt-get update && apt-get install git # on Debian/Ubuntu - $ sudo yum install git # on Fedora/Redhat/CentOS - $ sudo zypper install git # on openSUSE - $ sudo xbps-install git # on Void Linux - - - Create the directory where `SAGE_ROOT` should be established: - - $ mkdir -p ~/sage - $ cd ~/sage - - - Clone the Sage git repository: - - $ git clone -c core.symlinks=true --filter blob:none \ - --origin upstream --branch develop --tags \ - https://github.com/sagemath/sage.git - - This command obtains the most recent development release. - Replace `--branch develop` by `--branch master` to select - the most recent stable release instead. - - This will create the subdirectory `~/sage/sage`. (See the section - [Setting up git](https://doc.sagemath.org/html/en/developer/git_setup.html) - and the following sections in the Sage Developer's Guide - for more information.) - - - Change into the created subdirectory: - - $ cd sage - - - [Windows] The Sage source tree contains symbolic links, and the - build will not work if Windows line endings rather than UNIX - line endings are used. - - Therefore it is recommended (but not necessary) to use the - WSL version of `git`. - -3. Install system packages. - - Either refer for this to the [section on installation from - source](https://doc.sagemath.org/html/en/installation/source.html) in the - Sage Installation Manual for compilations of system packages - that you can install. When done, skip to step 7 (bootstrapping). - - Alternatively, follow the more fine-grained approach below. - -4. [Linux, WSL] Install the required minimal build prerequisites: - - - Compilers: `gcc`, `gfortran`, `g++` (GCC versions from 8.4.0 to 13.x - and recent versions of Clang (LLVM) are supported). - See [build/pkgs/gcc/SPKG.rst](build/pkgs/gcc/SPKG.rst) and - [build/pkgs/gfortran/SPKG.rst](build/pkgs/gfortran/SPKG.rst) - for a discussion of suitable compilers. - - - Build tools: GNU `make`, GNU `m4`, `perl` (including - `ExtUtils::MakeMaker`), `ranlib`, `git`, `tar`, `bc`. - See [build/pkgs/_prereq/SPKG.rst](build/pkgs/_prereq/SPKG.rst) for - more details. - - - Python 3.4 or later, or Python 2.7, a full installation including - `urllib`; but ideally version 3.9.x, 3.10.x, 3.11.x, 3.12.x, which - will avoid having to build Sage's own copy of Python 3. - See [build/pkgs/python3/SPKG.rst](build/pkgs/python3/SPKG.rst) - for more details. - - We have collected lists of system packages that provide these build - prerequisites. See, in the folder - [build/pkgs/_prereq/distros](build/pkgs/_prereq/distros), - the files - [arch.txt](build/pkgs/_prereq/distros/arch.txt), - [debian.txt](build/pkgs/_prereq/distros/debian.txt) - (also for Ubuntu, Linux Mint, etc.), - [fedora.txt](build/pkgs/_prereq/distros/fedora.txt) - (also for Red Hat, CentOS), - [opensuse.txt](build/pkgs/_prereq/distros/opensuse.txt), - [slackware.txt](build/pkgs/_prereq/distros/slackware.txt), and - [void.txt](build/pkgs/_prereq/distros/void.txt), or visit - https://doc.sagemath.org/html/en/reference/spkg/_prereq.html#spkg-prereq - -5. Optional: It is recommended that you have both LaTeX and - the ImageMagick tools (e.g. the "convert" command) installed - since some plotting functionality benefits from them. - -6. [Development] If you plan to do Sage development or otherwise work with - ticket branches and not only releases, install the bootstrapping - prerequisites. See the files in the folder - [build/pkgs/_bootstrap/distros](build/pkgs/_bootstrap/distros), or - visit - https://doc.sagemath.org/html/en/reference/spkg/_bootstrap.html#spkg-bootstrap - -7. Bootstrap the source tree using the following command: - - $ make configure - - (If the bootstrapping prerequisites are not installed, this command - will download a package providing pre-built bootstrap output instead.) - -8. Sanitize the build environment. Use the command - - $ env - - to inspect the current environment variables, in particular `PATH`, - `PKG_CONFIG_PATH`, `LD_LIBRARY_PATH`, `CFLAGS`, `CPPFLAGS`, `CXXFLAGS`, - and `LDFLAGS` (if set). - - Remove items from these (colon-separated) environment variables - that Sage should not use for its own build. In particular, remove - items if they refer to a previous Sage installation. - - - [WSL] In particular, WSL imports many items from the Windows - `PATH` variable into the Linux environment, which can lead to - confusing build errors. These items typically start with `/mnt/c`. - It is best to remove all of them from the environment variables. - For example, you can set `PATH` using the command: - - $ export PATH=/usr/sbin/:/sbin/:/bin/:/usr/lib/wsl/lib/ - - - [macOS with homebrew] Set required environment variables for the build: - - $ source ./.homebrew-build-env - - This is to make some of Homebrew's packages (so-called keg-only - packages) available for the build. Run it once to apply the - suggestions for the current terminal session. You may need to - repeat this command before you rebuild Sage from a new terminal - session, or after installing additional homebrew packages. (You - can also add it to your shell profile so that it gets run - automatically in all future sessions.) - -9. Optionally, decide on the installation prefix (`SAGE_LOCAL`): - - - Traditionally, and by default, Sage is installed into the - subdirectory hierarchy rooted at `SAGE_ROOT/local/`. - - - This can be changed using `./configure --prefix=SAGE_LOCAL`, - where `SAGE_LOCAL` is the desired installation prefix, which - must be writable by the user. - - If you use this option in combination with `--disable-editable`, - you can delete the entire Sage source tree after completing - the build process. What is installed in `SAGE_LOCAL` will be - a self-contained installation of Sage. - - - Note that in Sage's build process, `make` builds **and** - installs (`make install` is a no-op). Therefore the - installation hierarchy must be writable by the user. - - - See the Sage Installation Manual for options if you want to - install into shared locations such as `/usr/local/`. - Do not attempt to build Sage as `root`. - -10. Optionally, review the configuration options, which includes - many optional packages: - - $ ./configure --help - - Notable options for Sage developers are the following: - - - Use the option `--config-cache` to have `configure` - keep a disk cache of configuration values. This gives a nice speedup - when trying out ticket branches that make package upgrades, which - involves automatic re-runs of the configuration step. - - - Use the option `--enable-ccache` to have Sage install and use the - optional package `ccache`, which is preconfigured to keep a - disk cache of object files created from source files. This can give - a great speedup when switching between different branches, at the - expense of disk space use. - -11. Optional, but highly recommended: Set some environment variables to + - For a minimal installation, it is enough to have the following: + - Compilers: `gcc`, `gfortran`, `g++` + - Build tools: GNU `make`, GNU `m4`, `perl` (including `ExtUtils::MakeMaker`), `ranlib`, `git`, `tar`, `bc`. + - For a complete installation (recommended), see the linux system packages [in this guide](https://doc.sagemath.org/html/en/installation/source.html) +2. Download Sage: + - Open a terminal at the target folder and clone the Sage files there: `git clone https://github.com/bodnalev/sage.git` + - Move inside the sage source files with the command `cd sage` +3. Optional, but highly recommended: Set some environment variables to customize the build. - + For example, the `MAKE` environment variable controls whether to run several jobs in parallel. On a machine with 4 processors, say, typing `export MAKE="make -j4"` will configure the build script to perform a parallel compilation of Sage using 4 jobs. On some powerful machines, you might even consider `-j16`, as building with more jobs than CPU cores can speed things up further. - + To reduce the terminal output during the build, type `export V=0`. (`V` stands for "verbosity".) - - Some environment variables deserve a special mention: `CC`, - `CXX` and `FC`. These variables defining your compilers - can be set at configuration time and their values will be recorded for - further use at build time and runtime. - - For an in-depth discussion of more environment variables for - building Sage, see [the installation - guide](https://doc.sagemath.org/html/en/installation/source.html#environment-variables). - -12. Type `./configure`, followed by any options that you wish to use. - For example, to build Sage with `gf2x` package supplied by Sage, - use `./configure --with-system-gf2x=no`. - - At the end of a successful `./configure` run, you may see messages - recommending to install extra system packages using your package - manager. - - For a large [list of Sage - packages](https://github.com/sagemath/sage/issues/27330), Sage is able to - detect whether an installed system package is suitable for use with - Sage; in that case, Sage will not build another copy from source. - - Sometimes, the messages will recommend to install packages that are - already installed on your system. See the earlier configure - messages or the file `config.log` for explanation. Also, the - messages may recommend to install packages that are actually not - available; only the most recent releases of your distribution will - have all of these recommended packages. - -13. Optional: If you choose to install the additional system packages, - a re-run of `./configure` will test whether the versions installed - are usable for Sage; if they are, this will reduce the compilation - time and disk space needed by Sage. The usage of packages may be - adjusted by `./configure` parameters (check again the output of - `./configure --help`). - -14. Type `make`. That's it! Everything is automatic and - non-interactive. - - If you followed the above instructions, in particular regarding the - installation of system packages recommended by the output of - `./configure` (step 11), and regarding the parallel build (step 10), - building Sage takes less than one hour on a modern computer. - (Otherwise, it can take much longer.) - - The build should work fine on all fully supported platforms. If it - does not, we want to know! - -15. Type `./sage` to try it out. In Sage, try for example `2 + 2`, +4. Configure the build process with the command `make configure`. Then type `make build`. +5. Type `./sage` to try it out. In Sage, try for example `2 + 2`, `plot(x^2)`, `plot3d(lambda x, y: x*y, (-1, 1), (-1, 1))` to test a simple computation and plotting in 2D and 3D. Type Ctrl+D or `quit` to quit Sage. - -16. Optional: Type `make ptestlong` to test all examples in the documentation - (over 200,000 lines of input!) -- this takes from 10 minutes to - several hours. Don't get too disturbed if there are 2 to 3 failures, - but always feel free to email the section of `logs/ptestlong.log` that - contains errors to the [sage-support mailing list](https://groups.google.com/group/sage-support). - If there are numerous failures, there was a serious problem with your build. - -17. The HTML version of the [documentation](https://doc.sagemath.org/html/en/index.html) - is built during the compilation process of Sage and resides in the directory - `local/share/doc/sage/html/`. You may want to bookmark it in your browser. - -18. Optional: If you want to build the PDF version of the documentation, - run `make doc-pdf` (this requires LaTeX to be installed). - -19. Optional: Install optional packages of interest to you: - get a list by typing `./sage --optional` or by visiting the - [packages documentation page](https://doc.sagemath.org/html/en/reference/spkg/). - -20. Optional: Create a symlink to the installed `sage` script in a +6. Optional: Create a symlink to the installed `sage` script in a directory in your `PATH`, for example `/usr/local`. This will allow you to start Sage by typing `sage` from anywhere rather than having to either type the full path or navigate to the Sage @@ -423,248 +67,6 @@ in the Installation Guide. $ sudo ln -s $(./sage -sh -c 'ls $SAGE_ROOT/venv/bin/sage') /usr/local/bin -21. Optional: Set up SageMath as a Jupyter kernel in an existing Jupyter notebook - or JupyterLab installation, as described in the section - [Launching SageMath](https://doc.sagemath.org/html/en/installation/launching.html) - in the Sage Installation Guide. - -Alternative Installation using PyPI ---------------- - -For installing Sage in a Python environment from PyPI, Sage provides the -`pip`-installable package [sagemath-standard](https://pypi.org/project/sagemath-standard/). - -Unless you need to install Sage into a specific existing environment, we recommend -to create and activate a fresh virtual environment, for example `~/sage-venv/`: - - $ python3 -m venv ~/sage-venv - $ source ~/sage-venv/bin/activate - -As the first installation step, install [sage_conf](https://pypi.org/project/sage-conf/), -which builds various prerequisite packages in a subdirectory of `~/.sage/`: - - (sage-venv) $ python3 -m pip install -v sage_conf - -After a successful installation, a wheelhouse provides various Python packages. -You can list the wheels using the command: - - (sage-venv) $ ls $(sage-config SAGE_SPKG_WHEELS) - -If this gives an error saying that `sage-config` is not found, check any messages -that the `pip install` command may have printed. You may need to adjust your `PATH`, -for example by: - - $ export PATH="$(python3 -c 'import sysconfig; print(sysconfig.get_path("scripts", "posix_user"))'):$PATH" - -Now install the packages from the wheelhouse and the [sage_setup](https://pypi.org/project/sage-conf/) -package, and finally install the Sage library: - - (sage-venv) $ python3 -m pip install $(sage-config SAGE_SPKG_WHEELS)/*.whl sage_setup - (sage-venv) $ python3 -m pip install --no-build-isolation -v sagemath-standard - -The above instructions install the latest stable release of Sage. -To install the latest development version instead, add the switch `--pre` to all invocations of -`python3 -m pip install`. - -**NOTE:** PyPI has various other `pip`-installable packages with the word "sage" in their names. -Some of them are maintained by the SageMath project, some are provided by SageMath users for -various purposes, and others are entirely unrelated to SageMath. Do not use the packages -`sage` and `sagemath`. For a curated list of packages, see the chapter -[Packages and Features](https://doc.sagemath.org/html/en/reference/spkg/index.html) of the -Sage Reference Manual. - -SageMath Docker images ----------------------- - -[![Docker Status](http://dockeri.co/image/sagemath/sagemath)](https://hub.docker.com/r/sagemath/sagemath) - -SageMath is available on Docker Hub and can be downloaded by: -``` bash -docker pull sagemath/sagemath -``` - -Currently, only stable versions are kept up to date. - -Troubleshooting ---------------- - -If you have problems building Sage, check the Sage Installation Guide, -as well as the version-specific installation help in the [release -tour](https://github.com/sagemath/sage/releases) corresponding to the -version that you are installing. - -Please do not hesitate to ask for help in the [SageMath forum -](https://ask.sagemath.org/questions/) or the [sage-support mailing -list](https://groups.google.com/forum/#!forum/sage-support). The -[Troubleshooting section in the Sage Installation Guide -](https://doc.sagemath.org/html/en/installation/troubles.html) -provides instructions on what information to provide so that we can provide -help more effectively. - -Contributing to Sage --------------------- - -If you'd like to contribute to Sage, we strongly recommend that you read the -[Developer's Guide](https://doc.sagemath.org/html/en/developer/index.html). - -Sage has significant components written in the following languages: -C/C++, Python, Cython, Common Lisp, Fortran, and a bit of Perl. - -Directory Layout ----------------- - -Simplified directory layout (only essential files/directories): -``` -SAGE_ROOT Root directory (create by git clone) -├── build -│ └── pkgs Every package is a subdirectory here -│ ├── 4ti2/ -│ … -│ └── zlib/ -├── configure Top-level configure script -├── COPYING.txt Copyright information -├── pkgs Source trees of Python distribution packages -│ ├── sage-conf -│ │ ├── sage_conf.py -│ │ └── setup.py -│ ├── sage-docbuild -│ │ ├── sage_docbuild/ -│ │ └── setup.py -│ ├── sage-setup -│ │ ├── sage_setup/ -│ │ └── setup.py -│ ├── sage-sws2rst -│ │ ├── sage_sws2rst/ -│ │ └── setup.py -│ └── sagemath-standard -│ ├── bin/ -│ ├── sage -> ../../src/sage -│ └── setup.py -├── local (SAGE_LOCAL) Installation hierarchy for non-Python packages -│ ├── bin Executables -│ ├── include C/C++ headers -│ ├── lib Shared libraries, architecture-dependent data -│ ├── share Databases, architecture-independent data, docs -│ │ └── doc Viewable docs of Sage and of some components -│ └── var -│ ├── lib/sage -│ │ ├── installed/ -│ │ │ Records of installed non-Python packages -│ │ ├── scripts/ Scripts for uninstalling installed packages -│ │ └── venv-python3.9 (SAGE_VENV) -│ │ │ Installation hierarchy (virtual environment) -│ │ │ for Python packages -│ │ ├── bin/ Executables and installed scripts -│ │ ├── lib/python3.9/site-packages/ -│ │ │ Python modules/packages are installed here -│ │ └── var/lib/sage/ -│ │ └── wheels/ -│ │ Python wheels for all installed Python packages -│ │ -│ └── tmp/sage/ Temporary files when building Sage -├── logs -│ ├── install.log Full install log -│ └── pkgs Build logs of individual packages -│ ├── alabaster-0.7.12.log -│ … -│ └── zlib-1.2.11.log -├── m4 M4 macros for generating the configure script -│ └── *.m4 -├── Makefile Running "make" uses this file -├── prefix -> SAGE_LOCAL Convenience symlink to the installation tree -├── README.md This file -├── sage Script to start Sage -├── src Monolithic Sage library source tree -│ ├── bin/ Scripts that Sage uses internally -│ ├── doc/ Sage documentation sources -│ └── sage/ The Sage library source code -├── upstream Source tarballs of packages -│ ├── Babel-2.9.1.tar.gz -│ … -│ └── zlib-1.2.11.tar.gz -├── venv -> SAGE_VENV Convenience symlink to the virtual environment -└── VERSION.txt -``` -For more details see [our Developer's Guide](https://doc.sagemath.org/html/en/developer/coding_basics.html#files-and-directory-structure). - -Build System ------------- - -This is a brief summary of the Sage software distribution's build system. -There are two components to the full Sage system--the Sage Python library -and its associated user interfaces, and the larger software distribution of -Sage's main dependencies (for those dependencies not supplied by the user's -system). - -Sage's Python library is built and installed using a `setup.py` script as is -standard for Python packages (Sage's `setup.py` is non-trivial, but not -unusual). - -Most of the rest of the build system is concerned with building all of Sage's -dependencies in the correct order in relation to each other. The dependencies -included by Sage are referred to as SPKGs (i.e. "Sage Packages") and are listed -under `build/pkgs`. - -The main entrypoint to Sage's build system is the top-level `Makefile` at the -root of the source tree. Unlike most normal projects that use autoconf (Sage -does as well, as described below), this `Makefile` is not generated. Instead, -it contains a few high-level targets and targets related to bootstrapping the -system. Nonetheless, we still run `make ` from the root of the source -tree--targets not explicitly defined in the top-level `Makefile` are passed -through to another Makefile under `build/make/Makefile`. - -The latter `build/make/Makefile` *is* generated by an autoconf-generated -`configure` script, using the template in `build/make/Makefile.in`. This -includes rules for building the Sage library itself (`make sagelib`), and for -building and installing each of Sage's dependencies (e.g. `make gf2x`). - -The `configure` script itself, if it is not already built, can be generated by -running the `bootstrap` script (the latter requires _GNU autotools_ being installed). -The top-level `Makefile` also takes care of this automatically. - -To summarize, running a command like `make python3` at the top-level of the -source tree goes something like this: - -1. `make python3` -2. run `./bootstrap` if `configure` needs updating -3. run `./configure` with any previously configured options if `build/make/Makefile` - needs updating -4. change directory into `build/make` and run the `install` script--this is - little more than a front-end to running `make -f build/make/Makefile python3`, - which sets some necessary environment variables and logs some information -5. `build/make/Makefile` contains the actual rule for building `python3`; this - includes building all of `python3`'s dependencies first (and their - dependencies, recursively); the actual package installation is performed - with the `sage-spkg` program - -Relocation ----------- - -It is not supported to move the `SAGE_ROOT` or `SAGE_LOCAL` directory -after building Sage. If you do move the directories, you will have to -run ``make distclean`` and build Sage again from scratch. - -For a system-wide installation, you have to build Sage as a "normal" user -and then as root you can change permissions. See the [Installation Guide](https://doc.sagemath.org/html/en/installation/source.html#installation-in-a-multiuser-environment) -for further information. - -Redistribution --------------- - -Your local Sage install is almost exactly the same as any "developer" -install. You can make changes to documentation, source, etc., and very -easily package the complete results up for redistribution just like we -do. - -1. To make a binary distribution with your currently installed packages, - visit [sagemath/binary-pkg](https://github.com/sagemath/binary-pkg). - -2. To make your own source tarball of Sage, type: - - $ make dist - - The result is placed in the directory `dist/`. - Changes to Included Software ---------------------------- diff --git a/build/pkgs/blisspy/SPKG.rst b/build/pkgs/blisspy/SPKG.rst new file mode 100644 index 00000000000..98a2e9f1360 --- /dev/null +++ b/build/pkgs/blisspy/SPKG.rst @@ -0,0 +1,24 @@ +blisspy: Python Wrapper for Bliss Graph Library +=============================================== + +Description +----------- +`blisspy` is a Python wrapper for the Bliss graph library, providing functionalities to compute canonical forms and automorphism groups of graphs. + +License +------- +GNU General Public License v3.0 + +Upstream Contact +---------------- +[GitHub repository](https://github.com/bodnalev/blisspy) + +Dependencies +------------ +- Cython >=0.29 +- cysignals + +Special Update/Build Instructions +---------------------------------- +None. + diff --git a/build/pkgs/blisspy/checksums.ini b/build/pkgs/blisspy/checksums.ini new file mode 100644 index 00000000000..482def9ee8d --- /dev/null +++ b/build/pkgs/blisspy/checksums.ini @@ -0,0 +1,4 @@ +tarball=blisspy-VERSION.tar.gz +sha1=467b066ff1e189aeae7921df83ec3c1ad33b17ee +sha256=3e5e26c04b6fd41f08cb50e187265b9de1d15cfa695a4fe69a4623cc1ce8a967 +upstream_url=https://github.com/bodnalev/blisspy/raw/refs/heads/master/dist/blisspy-VERSION.tar.gz diff --git a/build/pkgs/blisspy/dependencies b/build/pkgs/blisspy/dependencies new file mode 100644 index 00000000000..acf79d4997e --- /dev/null +++ b/build/pkgs/blisspy/dependencies @@ -0,0 +1 @@ +cysignals | $(PYTHON_TOOLCHAIN) cython $(PYTHON) diff --git a/build/pkgs/blisspy/package-version.txt b/build/pkgs/blisspy/package-version.txt new file mode 100644 index 00000000000..49d59571fbf --- /dev/null +++ b/build/pkgs/blisspy/package-version.txt @@ -0,0 +1 @@ +0.1 diff --git a/build/pkgs/blisspy/spkg-configure.m4 b/build/pkgs/blisspy/spkg-configure.m4 new file mode 100644 index 00000000000..219b3f2d818 --- /dev/null +++ b/build/pkgs/blisspy/spkg-configure.m4 @@ -0,0 +1 @@ +SAGE_SPKG_CONFIGURE([blisspy], [SAGE_PYTHON_PACKAGE_CHECK([blisspy])]) diff --git a/build/pkgs/blisspy/spkg-install.in b/build/pkgs/blisspy/spkg-install.in new file mode 100644 index 00000000000..37ac1a53437 --- /dev/null +++ b/build/pkgs/blisspy/spkg-install.in @@ -0,0 +1,2 @@ +cd src +sdh_pip_install . diff --git a/build/pkgs/blisspy/type b/build/pkgs/blisspy/type new file mode 100644 index 00000000000..a6a7b9cd726 --- /dev/null +++ b/build/pkgs/blisspy/type @@ -0,0 +1 @@ +standard diff --git a/build/pkgs/blisspy/version_requirements.txt b/build/pkgs/blisspy/version_requirements.txt new file mode 100644 index 00000000000..6e6c2ffc8af --- /dev/null +++ b/build/pkgs/blisspy/version_requirements.txt @@ -0,0 +1,2 @@ +Cython >=0.29 +cysignals diff --git a/build/pkgs/csdpy/SPKG.rst b/build/pkgs/csdpy/SPKG.rst new file mode 100644 index 00000000000..608a5580bd7 --- /dev/null +++ b/build/pkgs/csdpy/SPKG.rst @@ -0,0 +1,24 @@ +csdpy: Python Wrapper for the Coin-Or project's CSDP solver +=============================================== + +Description +----------- +`csdpy` is a Python wrapper for the CSDP library, providing a fast algorithm to solve SDP problems. + +License +------- +GNU General Public License v3.0 + +Upstream Contact +---------------- +[GitHub repository](https://github.com/bodnalev/csdpy) + +Dependencies +------------ +- Cython >=0.29 +- cysignals + +Special Update/Build Instructions +---------------------------------- +None. + diff --git a/build/pkgs/csdpy/checksums.ini b/build/pkgs/csdpy/checksums.ini new file mode 100644 index 00000000000..5d4eca933f0 --- /dev/null +++ b/build/pkgs/csdpy/checksums.ini @@ -0,0 +1,4 @@ +tarball=csdpy-VERSION.tar.gz +sha1=1c1b0f0d5c3f67dd28f7f16cba933ed1357266af +sha256=e84c13ba5994c3fbfd1fef36a60ed05a3a382b7b4402f2968c77afcc276779f2 +upstream_url=https://github.com/bodnalev/csdpy/raw/refs/heads/master/dist/csdpy-VERSION.tar.gz diff --git a/build/pkgs/csdpy/dependencies b/build/pkgs/csdpy/dependencies new file mode 100644 index 00000000000..1690f4be775 --- /dev/null +++ b/build/pkgs/csdpy/dependencies @@ -0,0 +1 @@ +$(PYTHON_TOOLCHAIN) cython $(PYTHON) diff --git a/build/pkgs/csdpy/package-version.txt b/build/pkgs/csdpy/package-version.txt new file mode 100644 index 00000000000..49d59571fbf --- /dev/null +++ b/build/pkgs/csdpy/package-version.txt @@ -0,0 +1 @@ +0.1 diff --git a/build/pkgs/csdpy/spkg-configure.m4 b/build/pkgs/csdpy/spkg-configure.m4 new file mode 100644 index 00000000000..150f72277d5 --- /dev/null +++ b/build/pkgs/csdpy/spkg-configure.m4 @@ -0,0 +1 @@ +SAGE_SPKG_CONFIGURE([csdpy], [SAGE_PYTHON_PACKAGE_CHECK([csdpy])]) diff --git a/build/pkgs/csdpy/spkg-install b/build/pkgs/csdpy/spkg-install new file mode 100644 index 00000000000..980c8796fed --- /dev/null +++ b/build/pkgs/csdpy/spkg-install @@ -0,0 +1,20 @@ +#!/bin/sh +if [ "$SAGE_LOCAL" = "" ]; then + echo "SAGE_LOCAL is not set. Exiting." + exit 1 +fi +GITHUB_FILES="https://github.com/bodnalev/csdpy/archive/refs/heads/master.zip" +TMP_DIR=$(mktemp -d) +wget -O "$TMP_DIR/csdpy.zip" "$GITHUB_FILES" || exit 1 +unzip "$TMP_DIR/csdpy.zip" -d "$TMP_DIR" || exit 1 +cd "$TMP_DIR/csdpy-master" || exit 1 +OS_TYPE=$(uname) +if [ "$OS_TYPE" = "Linux" ]; then + "$SAGE_LOCAL/bin/python3" setup_linux.py install || exit 1 +elif [ "$OS_TYPE" = "Darwin" ]; then + "$SAGE_LOCAL/bin/python3" setup_mac.py install || exit 1 +else + echo "Unsupported OS: $OS_TYPE. Exiting." + exit 1 +fi +rm -rf "$TMP_DIR" \ No newline at end of file diff --git a/build/pkgs/csdpy/spkg-install.in b/build/pkgs/csdpy/spkg-install.in new file mode 100644 index 00000000000..37ac1a53437 --- /dev/null +++ b/build/pkgs/csdpy/spkg-install.in @@ -0,0 +1,2 @@ +cd src +sdh_pip_install . diff --git a/build/pkgs/csdpy/type b/build/pkgs/csdpy/type new file mode 100644 index 00000000000..a6a7b9cd726 --- /dev/null +++ b/build/pkgs/csdpy/type @@ -0,0 +1 @@ +standard diff --git a/build/pkgs/csdpy/version_requirements.txt b/build/pkgs/csdpy/version_requirements.txt new file mode 100644 index 00000000000..6e6c2ffc8af --- /dev/null +++ b/build/pkgs/csdpy/version_requirements.txt @@ -0,0 +1,2 @@ +Cython >=0.29 +cysignals diff --git a/build/pkgs/sagelib/dependencies b/build/pkgs/sagelib/dependencies index b1ebfd0825e..b5a9961ed00 100644 --- a/build/pkgs/sagelib/dependencies +++ b/build/pkgs/sagelib/dependencies @@ -1,4 +1,4 @@ -FORCE $(SCRIPTS) boost_cropped $(BLAS) brial cliquer cypari cysignals cython ecl eclib ecm flint libgd gap giac givaro glpk gmpy2 gsl iml importlib_metadata importlib_resources jupyter_core lcalc lrcalc_python libbraiding libhomfly libpng linbox m4ri m4rie memory_allocator mpc mpfi mpfr $(MP_LIBRARY) ntl numpy pari pip pkgconfig planarity ppl pplpy primesieve primecount primecountpy $(PYTHON) requests rw sage_conf singular symmetrica typing_extensions $(PCFILES) | $(PYTHON_TOOLCHAIN) sage_setup $(PYTHON) pythran +FORCE $(SCRIPTS) boost_cropped $(BLAS) blisspy brial cliquer cypari cysignals cython ecl eclib ecm flint libgd gap giac givaro glpk gmpy2 gsl iml importlib_metadata importlib_resources jupyter_core lcalc lrcalc_python libbraiding libhomfly libpng linbox m4ri m4rie memory_allocator mpc mpfi mpfr $(MP_LIBRARY) ntl numpy pari pip pkgconfig planarity ppl pplpy primesieve primecount primecountpy $(PYTHON) requests rw sage_conf singular symmetrica typing_extensions $(PCFILES) | $(PYTHON_TOOLCHAIN) sage_setup $(PYTHON) pythran ---------- All lines of this file are ignored except the first. diff --git a/build/pkgs/tqdm/SPKG.rst b/build/pkgs/tqdm/SPKG.rst new file mode 100644 index 00000000000..c0770977d53 --- /dev/null +++ b/build/pkgs/tqdm/SPKG.rst @@ -0,0 +1,24 @@ +tqdm: Fast, Extensible Progress Meter +=============================================== + +Description +----------- +tqdm derives from the Arabic word taqaddum (تقدّم) which can mean “progress,” and is an abbreviation for “I love you so much” in Spanish (te quiero demasiado). + +Instantly make your loops show a smart progress meter - just wrap any iterable with tqdm(iterable), and you’re done! + +License +------- +MPL 2.0 and MIT + +Upstream Contact +---------------- +[Official PyPi page](https://pypi.org/project/tqdm/) + +Dependencies +------------ + +Special Update/Build Instructions +---------------------------------- +None. + diff --git a/build/pkgs/tqdm/checksums.ini b/build/pkgs/tqdm/checksums.ini new file mode 100644 index 00000000000..632dfa5c814 --- /dev/null +++ b/build/pkgs/tqdm/checksums.ini @@ -0,0 +1,4 @@ +tarball=tqdm-VERSION.tar.gz +sha1=e70c395f3cd6ad596a4d6d27b1284a7d377d5048 +sha256=f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2 +upstream_url=https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-VERSION.tar.gz diff --git a/build/pkgs/tqdm/dependencies b/build/pkgs/tqdm/dependencies new file mode 100644 index 00000000000..ca33204bd52 --- /dev/null +++ b/build/pkgs/tqdm/dependencies @@ -0,0 +1 @@ + | $(PYTHON_TOOLCHAIN) $(PYTHON) diff --git a/build/pkgs/tqdm/package-version.txt b/build/pkgs/tqdm/package-version.txt new file mode 100644 index 00000000000..f538bef708d --- /dev/null +++ b/build/pkgs/tqdm/package-version.txt @@ -0,0 +1 @@ +4.67.1 diff --git a/build/pkgs/tqdm/spkg-configure.m4 b/build/pkgs/tqdm/spkg-configure.m4 new file mode 100644 index 00000000000..ff66a23eadf --- /dev/null +++ b/build/pkgs/tqdm/spkg-configure.m4 @@ -0,0 +1 @@ +SAGE_SPKG_CONFIGURE([tqdm], [SAGE_PYTHON_PACKAGE_CHECK([tqdm])]) diff --git a/build/pkgs/tqdm/spkg-install b/build/pkgs/tqdm/spkg-install new file mode 100755 index 00000000000..23dda9803ec --- /dev/null +++ b/build/pkgs/tqdm/spkg-install @@ -0,0 +1,8 @@ +#!/bin/sh +if [ "$SAGE_LOCAL" = "" ]; then + echo "SAGE_LOCAL is not set. Exiting." + exit 1 +fi + +# Install the latest version of tqdm from PyPI +"$SAGE_LOCAL/bin/pip" install tqdm diff --git a/build/pkgs/tqdm/spkg-install.in b/build/pkgs/tqdm/spkg-install.in new file mode 100644 index 00000000000..37ac1a53437 --- /dev/null +++ b/build/pkgs/tqdm/spkg-install.in @@ -0,0 +1,2 @@ +cd src +sdh_pip_install . diff --git a/build/pkgs/tqdm/type b/build/pkgs/tqdm/type new file mode 100644 index 00000000000..a6a7b9cd726 --- /dev/null +++ b/build/pkgs/tqdm/type @@ -0,0 +1 @@ +standard diff --git a/flag_tutorial.ipynb b/flag_tutorial.ipynb new file mode 100644 index 00000000000..43c1ac4972f --- /dev/null +++ b/flag_tutorial.ipynb @@ -0,0 +1,3286 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "dbd830bd-b428-4786-887c-da759b5217d4", + "metadata": {}, + "source": [ + "\n", + "Short guide to the flag algebra package\n", + "=======================================\n", + "\n", + "Contents:\n", + "1. Quick start\n", + "2. Flags\n", + " 1. Creating flags\n", + " 2. Everything is induced\n", + " 3. Operations over flags\n", + " 4. Types\n", + " 5. Patterns\n", + "3. Theories\n", + " 1. Already present theories\n", + " 4. Creating new theories\n", + " 2. Excluding things\n", + " 3. Generating things\n", + " 5. Combining theories\n", + "4. Optimizing\n", + " 1. Assumptions\n", + " 2. Rounding\n", + " 3. Construction\n", + " 4. Certificates\n", + " 6. Exporting the SDP problem\n", + " 8. Rounding over field extensions\n", + "5. Miscallenous" + ] + }, + { + "cell_type": "markdown", + "id": "499b2984-9356-461b-9299-c55cc50d41e1", + "metadata": {}, + "source": [ + "Quick start\n", + "===========\n", + "\n", + "Here Mantel's theorem will be demonstrated" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "0a2fb8ed-2cf2-4703-865a-81355945e997", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Base flags generated, their number is 3\n", + "The relevant ftypes are constructed, their number is 1\n", + "Block sizes before symmetric/asymmetric change is applied: [2]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with mult table for Ftype on 1 points with edges=(): : 1it [00:00, 497.07it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tables finished\n", + "Constraints finished\n", + "Running sdp without construction. Used block sizes are [2, -3, -2]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CSDP 6.2.0\n", + "Iter: 0 Ap: 0.00e+00 Pobj: 0.0000000e+00 Ad: 0.00e+00 Dobj: 0.0000000e+00 \n", + "Iter: 1 Ap: 1.00e+00 Pobj: -1.5751371e+01 Ad: 7.93e-01 Dobj: -1.8596991e-01 \n", + "Iter: 2 Ap: 1.00e+00 Pobj: -1.3829292e+01 Ad: 9.44e-01 Dobj: -2.5639377e-01 \n", + "Iter: 3 Ap: 1.00e+00 Pobj: -3.6548137e+00 Ad: 9.22e-01 Dobj: -2.8097573e-01 \n", + "Iter: 4 Ap: 1.00e+00 Pobj: -6.7094195e-01 Ad: 8.42e-01 Dobj: -3.1080901e-01 \n", + "Iter: 5 Ap: 1.00e+00 Pobj: -5.8576089e-01 Ad: 8.47e-01 Dobj: -4.8072710e-01 \n", + "Iter: 6 Ap: 1.00e+00 Pobj: -5.0662032e-01 Ad: 8.78e-01 Dobj: -4.9275677e-01 \n", + "Iter: 7 Ap: 1.00e+00 Pobj: -5.0069959e-01 Ad: 9.33e-01 Dobj: -4.9911657e-01 \n", + "Iter: 8 Ap: 1.00e+00 Pobj: -5.0005821e-01 Ad: 1.00e+00 Dobj: -4.9996202e-01 \n", + "Iter: 9 Ap: 1.00e+00 Pobj: -5.0000265e-01 Ad: 1.00e+00 Dobj: -4.9999892e-01 \n", + "Iter: 10 Ap: 1.00e+00 Pobj: -5.0000020e-01 Ad: 1.00e+00 Dobj: -4.9999999e-01 \n", + "Iter: 11 Ap: 1.00e+00 Pobj: -5.0000001e-01 Ad: 9.90e-01 Dobj: -5.0000000e-01 \n", + "Iter: 12 Ap: 9.57e-01 Pobj: -5.0000000e-01 Ad: 9.69e-01 Dobj: -5.0000000e-01 \n", + "Success: SDP solved\n", + "Primal objective value: -5.0000000e-01 \n", + "Dual objective value: -5.0000000e-01 \n", + "Relative primal infeasibility: 1.13e-15 \n", + "Relative dual infeasibility: 1.18e-10 \n", + "Real Relative Gap: 2.59e-10 \n", + "XZ Relative Gap: 4.63e-10 \n", + "DIMACS error measures: 1.18e-15 0.00e+00 2.46e-10 0.00e+00 2.59e-10 4.63e-10\n", + "The initial run gave an accurate looking construction\n", + "Rounded construction vector is: \n", + "Flag Algebra Element over Rational Field\n", + "1/4 - Flag on 3 points, ftype from () with edges=()\n", + "0 - Flag on 3 points, ftype from () with edges=(01)\n", + "3/4 - Flag on 3 points, ftype from () with edges=(01 02)\n", + "Adjusting table with kernels from construction\n", + "Running SDP after kernel correction. Used block sizes are [1, -3, -2]\n", + "CSDP 6.2.0\n", + "Iter: 0 Ap: 0.00e+00 Pobj: 0.0000000e+00 Ad: 0.00e+00 Dobj: 0.0000000e+00 \n", + "Iter: 1 Ap: 1.00e+00 Pobj: -1.2107440e+01 Ad: 8.40e-01 Dobj: 6.1913653e-01 \n", + "Iter: 2 Ap: 1.00e+00 Pobj: -1.0543280e+01 Ad: 9.38e-01 Dobj: -1.3876525e-01 \n", + "Iter: 3 Ap: 1.00e+00 Pobj: -2.4595774e+00 Ad: 9.42e-01 Dobj: -1.9392703e-01 \n", + "Iter: 4 Ap: 9.51e-01 Pobj: -6.1484578e-01 Ad: 8.50e-01 Dobj: -2.3934806e-01 \n", + "Iter: 5 Ap: 1.00e+00 Pobj: -6.4529870e-01 Ad: 7.42e-01 Dobj: -4.8596320e-01 \n", + "Iter: 6 Ap: 9.78e-01 Pobj: -5.0782840e-01 Ad: 8.89e-01 Dobj: -4.9454657e-01 \n", + "Iter: 7 Ap: 9.90e-01 Pobj: -5.0036064e-01 Ad: 9.65e-01 Dobj: -4.9953666e-01 \n", + "Iter: 8 Ap: 1.00e+00 Pobj: -5.0002547e-01 Ad: 1.00e+00 Dobj: -4.9997505e-01 \n", + "Iter: 9 Ap: 9.90e-01 Pobj: -5.0000127e-01 Ad: 1.00e+00 Dobj: -4.9999949e-01 \n", + "Iter: 10 Ap: 1.00e+00 Pobj: -5.0000006e-01 Ad: 1.00e+00 Dobj: -4.9999997e-01 \n", + "Iter: 11 Ap: 9.70e-01 Pobj: -5.0000000e-01 Ad: 9.70e-01 Dobj: -5.0000000e-01 \n", + "Success: SDP solved\n", + "Primal objective value: -5.0000000e-01 \n", + "Dual objective value: -5.0000000e-01 \n", + "Relative primal infeasibility: 1.25e-15 \n", + "Relative dual infeasibility: 1.57e-09 \n", + "Real Relative Gap: 1.08e-09 \n", + "XZ Relative Gap: 3.47e-09 \n", + "DIMACS error measures: 1.31e-15 0.00e+00 3.20e-09 0.00e+00 1.08e-09 3.47e-09\n", + "Starting the rounding of the result\n", + "Flattening X matrices\n", + "This took 0.15086054801940918s\n", + "Correcting flat X matrices\n", + "Dimensions: (2, 1)\n", + "This took 0.007645845413208008s\n", + "Unflattening X matrices\n", + "This took 0.0005834102630615234s\n", + "Calculating resulting bound\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|████████████████████████████████████████████| 1/1 [00:00<00:00, 250.54it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "This took 0.009990453720092773s\n", + "Final rounded bound is 1/2\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/plain": [ + "1/2" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Check Mantel's theorem\n", + "\n", + "# Define a triangle\n", + "triangle = GraphTheory(3, edges=[[0, 1], [0, 2], [1, 2]])\n", + "\n", + "# Define an edge\n", + "edge = GraphTheory(2, edges=[[0, 1]])\n", + "\n", + "# Exclude triangles\n", + "GraphTheory.exclude(triangle)\n", + "# Maximize edges, calculate up to size 3, make the result exact\n", + "GraphTheory.optimize(edge, 3, maximize=True, exact=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "6926a408-3bbe-486e-b90b-bcfa1512bd1a", + "metadata": {}, + "outputs": [], + "source": [ + "# Python syntax\n", + "# For short, use G for GraphTheory\n", + "G = GraphTheory" + ] + }, + { + "cell_type": "markdown", + "id": "120b1a0d-f003-495f-8de6-cfd0aefdf725", + "metadata": {}, + "source": [ + "Flags\n", + "=====\n", + "\n", + "1. Creating flags\n", + "2. Everything is induced\n", + "3. Operations with flags\n", + "4. Types\n", + "5. Patterns" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "3ae807f1-369f-495c-bcbe-bb7453cea306", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Is cherry the same as other cherry? True\n", + "Triangle flag is: Flag on 3 points, ftype from () with edges=(01 02 12)\n", + "The test flag is Flag on 3 points, ftype from () with edges=()\n" + ] + } + ], + "source": [ + "###\n", + "### Creating flags\n", + "###\n", + "\n", + "# To create a flag, write TheoryName(size, relation_name=...)\n", + "\n", + "# Example for graphs\n", + "triangle = G(3, edges=[[0, 1], [0, 2], [1, 2]])\n", + "cherry = G(3, edges=[[0, 1], [0, 2]])\n", + "# Example for three graphs\n", + "k4m = ThreeGraphTheory(4, edges=[[0, 1, 2], [0, 1, 3], [1, 2, 3]])\n", + "\n", + "# Note automorphism doesn't matter\n", + "other_cherry = G(3, edges=[[0, 1], [2, 1]])\n", + "print(\"Is cherry the same as other cherry? \", cherry==other_cherry)\n", + "\n", + "# Flags can be printed\n", + "print(\"Triangle flag is: \", triangle)\n", + "\n", + "# If a relation is not included, it is assumed to be empty\n", + "test = G(3)\n", + "print(\"The test flag is \", test)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "a19c1502-cbef-4d35-9611-466fb98d1465", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Base flags generated, their number is 3\n", + "The relevant ftypes are constructed, their number is 1\n", + "Block sizes before symmetric/asymmetric change is applied: [2]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with mult table for Ftype on 1 points with edges=(): : 1it [00:00, 9.70it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tables finished\n", + "Constraints finished\n", + "Running sdp without construction. Used block sizes are [2, -3, -2]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CSDP 6.2.0\n", + "Iter: 0 Ap: 0.00e+00 Pobj: 0.0000000e+00 Ad: 0.00e+00 Dobj: 0.0000000e+00 \n", + "Iter: 1 Ap: 1.00e+00 Pobj: -1.5751371e+01 Ad: 7.93e-01 Dobj: -1.8596991e-01 \n", + "Iter: 2 Ap: 1.00e+00 Pobj: -1.3829292e+01 Ad: 9.44e-01 Dobj: -2.5639377e-01 \n", + "Iter: 3 Ap: 1.00e+00 Pobj: -3.6548137e+00 Ad: 9.22e-01 Dobj: -2.8097573e-01 \n", + "Iter: 4 Ap: 1.00e+00 Pobj: -6.7101882e-01 Ad: 8.42e-01 Dobj: -3.1080817e-01 \n", + "Iter: 5 Ap: 1.00e+00 Pobj: -5.8576221e-01 Ad: 8.44e-01 Dobj: -4.8002517e-01 \n", + "Iter: 6 Ap: 1.00e+00 Pobj: -5.0647693e-01 Ad: 8.85e-01 Dobj: -4.9296282e-01 \n", + "Iter: 7 Ap: 1.00e+00 Pobj: -5.0060021e-01 Ad: 9.41e-01 Dobj: -4.9923796e-01 \n", + "Iter: 8 Ap: 1.00e+00 Pobj: -5.0005080e-01 Ad: 1.00e+00 Dobj: -4.9996691e-01 \n", + "Iter: 9 Ap: 1.00e+00 Pobj: -5.0000234e-01 Ad: 1.00e+00 Dobj: -4.9999913e-01 \n", + "Iter: 10 Ap: 1.00e+00 Pobj: -5.0000016e-01 Ad: 1.00e+00 Dobj: -5.0000000e-01 \n", + "Iter: 11 Ap: 9.58e-01 Pobj: -5.0000001e-01 Ad: 9.70e-01 Dobj: -5.0000000e-01 \n", + "Success: SDP solved\n", + "Primal objective value: -5.0000001e-01 \n", + "Dual objective value: -5.0000000e-01 \n", + "Relative primal infeasibility: 5.33e-16 \n", + "Relative dual infeasibility: 3.98e-09 \n", + "Real Relative Gap: 2.49e-09 \n", + "XZ Relative Gap: 9.08e-09 \n", + "DIMACS error measures: 5.58e-16 0.00e+00 8.28e-09 0.00e+00 2.49e-09 9.08e-09\n" + ] + }, + { + "data": { + "text/plain": [ + "0.5000000069458745" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "###\n", + "### Everything is induced\n", + "###\n", + "\n", + "# Note that everything is induced. Mantel's theorem can be checked with the complements\n", + "\n", + "G.reset()\n", + "G.exclude(G(3))\n", + "G.optimize(G(2), 3)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "9893cc17-27d3-49ee-b729-118f6cc83354", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(Flag Algebra Element over Rational Field\n", + " 1 - Flag on 3 points, ftype from () with edges=()\n", + " 0 - Flag on 3 points, ftype from () with edges=(01)\n", + " 1 - Flag on 3 points, ftype from () with edges=(01 02)\n", + " 0 - Flag on 3 points, ftype from () with edges=(01 02 12),\n", + " Flag Algebra Element over Rational Field\n", + " 1 - Flag on 4 points, ftype from () with edges=()\n", + " 2/3 - Flag on 4 points, ftype from () with edges=(01)\n", + " 1/3 - Flag on 4 points, ftype from () with edges=(01 03)\n", + " 2/3 - Flag on 4 points, ftype from () with edges=(02 13)\n", + " 1/3 - Flag on 4 points, ftype from () with edges=(01 02 13)\n", + " 1/3 - Flag on 4 points, ftype from () with edges=(02 03 12 13))" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "###\n", + "### Operations with flags\n", + "###\n", + "\n", + "\n", + "G.reset()\n", + "empty3 = G(3)\n", + "# Addition, Multiplication\n", + "empty3 + cherry, G(2)*G(2)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "0f4b29d7-6e5a-486c-b87b-cbf4f9dcd636", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Flag Algebra Element over Rational Field\n", + "1/6 - Flag on 4 points, ftype from () with edges=(01)\n", + "1/3 - Flag on 4 points, ftype from () with edges=(01 03)\n", + "1/3 - Flag on 4 points, ftype from () with edges=(02 13)\n", + "1/2 - Flag on 4 points, ftype from () with edges=(01 02 03)\n", + "1/2 - Flag on 4 points, ftype from () with edges=(01 03 13)\n", + "1/2 - Flag on 4 points, ftype from () with edges=(01 02 13)\n", + "2/3 - Flag on 4 points, ftype from () with edges=(01 02 03 13)\n", + "2/3 - Flag on 4 points, ftype from () with edges=(02 03 12 13)\n", + "5/6 - Flag on 4 points, ftype from () with edges=(01 02 03 12 13)\n", + "1 - Flag on 4 points, ftype from () with edges=(01 02 03 12 13 23)" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Chain rule, increase size by 1\n", + "k2 = G(2, edges=[[0, 1]])\n", + "# Increase size with << operator\n", + "k2 << 2" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "c37ef1f5-4be3-43e0-96d5-706c00833f77", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Flag on 2 points, ftype from (0,) with edges=(01)" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "###\n", + "### Types\n", + "###\n", + "\n", + "# Add ftype=[] list of points to define the marked vertices, when creating a flag\n", + "pointed_edge = G(2, edges=[[0, 1]], ftype=[0])\n", + "pointed_edge" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "35d47c34-9758-4fb7-9296-673703ca56f8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(False, False, False)" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Types must map to types (with the same order)\n", + "\n", + "fl0 = G(3, edges=[[0, 1]], ftype=[0, 2])\n", + "fl1 = G(3, edges=[[0, 1]], ftype=[2, 0])\n", + "fl2 = G(3, edges=[[0, 1]], ftype=[2])\n", + "fl0==fl1, fl0==fl2, fl1==fl2" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "cd8afb21-1df5-4482-8be6-059df12cba6e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Flag Algebra Element over Rational Field\n", + "0 - Flag on 3 points, ftype from (0,) with edges=()\n", + "1/2 - Flag on 3 points, ftype from (0,) with edges=(01)\n", + "0 - Flag on 3 points, ftype from (2,) with edges=(01)\n", + "2 - Flag on 3 points, ftype from (0,) with edges=(01 02)\n", + "1/2 - Flag on 3 points, ftype from (1,) with edges=(01 02)\n", + "1 - Flag on 3 points, ftype from (0,) with edges=(01 02 12)\n", + "Flag Algebra Element over Rational Field\n", + "0 - Flag on 3 points, ftype from (0,) with edges=()\n", + "0 - Flag on 3 points, ftype from (0,) with edges=(01)\n", + "0 - Flag on 3 points, ftype from (2,) with edges=(01)\n", + "1 - Flag on 3 points, ftype from (0,) with edges=(01 02)\n", + "0 - Flag on 3 points, ftype from (1,) with edges=(01 02)\n", + "1 - Flag on 3 points, ftype from (0,) with edges=(01 02 12)\n" + ] + } + ], + "source": [ + "# Operations still work\n", + "pointed_cherry = G(3, edges=[[0, 1], [1, 2]], ftype=[1])\n", + "print(pointed_edge + pointed_cherry)\n", + "print(pointed_edge*pointed_edge)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "d3b0ed9c-69c0-46e9-965e-b5f6831af09a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Flag Algebra Element over Rational Field\n", + "0 - Flag on 3 points, ftype from () with edges=()\n", + "0 - Flag on 3 points, ftype from () with edges=(01)\n", + "1/3 - Flag on 3 points, ftype from () with edges=(01 02)\n", + "0 - Flag on 3 points, ftype from () with edges=(01 02 12)" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# The averaging operator, called project here\n", + "pointed_cherry.project()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "c9ae6813-815b-416d-a11d-0c99cba78393", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Ftype on 2 points with edges=(01)" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# To create a type, simply create a flag with all vertices marked as type\n", + "edgetype = G(2, edges=[[0, 1]], ftype=[0, 1])\n", + "edgetype" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "f2f4795c-c0ea-4467-a4d4-f103f9a4292c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[Flag on 3 points, ftype from () with edges=(01),\n", + " Flag on 3 points, ftype from () with edges=(01 02),\n", + " Flag on 3 points, ftype from () with edges=(01 02 12)]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "###\n", + "### Patterns\n", + "###\n", + "\n", + "# A pattern is a non-induced flag. Every relation is optional, unless stated otherwise.\n", + "\n", + "# This induced graph has 3 points and an edge.\n", + "edge3 = G(3, edges=[[0, 1]])\n", + "# This pattern is the same, but non-induced.\n", + "pat3 = G.pattern(3, edges=[[0, 1]])\n", + "\n", + "# To see the list of induced flags compatible with this patter, call\n", + "pat3.compatible_flags()" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "a3f69265-c186-4d4d-9e02-b723a4ffc2d2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[Flag on 3 points, ftype from () with edges=(01),\n", + " Flag on 3 points, ftype from () with edges=(01 02)]" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# A pattern can have strictly missing relations too. We can specify by listing them with the _missing or _m suffix\n", + "\n", + "# This pattern on 3 points must have one edge present and one edge missing\n", + "mpat3 = G.pattern(3, edges=[[0, 1]], edges_missing=[[1, 2]])\n", + "mpat3.compatible_flags()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "dcc4c945-9709-44c6-a7db-1274f1b6d0fe", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Flag Algebra Element over Rational Field\n", + "0 - Flag on 3 points, ftype from () with edges=()\n", + "4/3 - Flag on 3 points, ftype from () with edges=(01)\n", + "5/3 - Flag on 3 points, ftype from () with edges=(01 02)\n", + "1 - Flag on 3 points, ftype from () with edges=(01 02 12)" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# When a pattern is used in an expression, it is just converted to the sum of it's elements\n", + "\n", + "edge + mpat3" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "98438562-8de7-4cc5-b67f-94496d9a1a27", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[Flag on 5 points, ftype from () with edges=(01 04 14),\n", + " Flag on 5 points, ftype from () with edges=(01 03 04 14),\n", + " Flag on 5 points, ftype from () with edges=(01 02 03 04 14),\n", + " Flag on 5 points, ftype from () with edges=(01 03 04 12 14),\n", + " Flag on 5 points, ftype from () with edges=(01 02 04 12 14),\n", + " Flag on 5 points, ftype from () with edges=(01 02 03 04 12 14),\n", + " Flag on 5 points, ftype from () with edges=(01 02 03 04 12 13 14)]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Patterns can be created with the shorted .p\n", + "# Also instead of adding _missing, it is enough to add _m\n", + "G.reset()\n", + "# 5 points, has a triangle and a missing triangle.\n", + "test = G.p(5, edges=[[0, 1], [1, 2], [0, 2]], edges_m=[[2, 3], [3, 4], [2, 4]])\n", + "test.compatible_flags()" + ] + }, + { + "cell_type": "markdown", + "id": "1857a500-de28-4618-aac7-0299cada29b5", + "metadata": {}, + "source": [ + "Theories\n", + "========\n", + "\n", + "1. Already present theories\n", + "4. Creating new theories\n", + "2. Excluding things\n", + "3. Generating things\n", + "5. Combining theories" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "208e31ba-c06d-46c0-9a9c-b963a4f0bc2c", + "metadata": {}, + "outputs": [], + "source": [ + "###\n", + "### Already present theories\n", + "###\n", + "\n", + "# The following theories are already created, the names are self explanatory\n", + "\n", + "GraphTheory = Theory(\"Graph\")\n", + "DiGraphTheory = Theory(\"DiGraph\", arity=2, is_ordered=True)\n", + "ThreeGraphTheory = Theory(\"ThreeGraph\", arity=3)\n", + "DiThreeGraphTheory = Theory(\"DiThreeGraph\", arity=3, is_ordered=True)\n", + "FourGraphTheory = Theory(\"FourGraph\", arity=4)\n", + "Color0 = Theory(\"Color0\", relation_name=\"C0\", arity=1)\n", + "Color1 = Theory(\"Color1\", relation_name=\"C1\", arity=1)\n", + "Color2 = Theory(\"Color2\", relation_name=\"C2\", arity=1)\n", + "Color3 = Theory(\"Color3\", relation_name=\"C3\", arity=1)\n", + "Color4 = Theory(\"Color4\", relation_name=\"C4\", arity=1)\n", + "Color5 = Theory(\"Color5\", relation_name=\"C5\", arity=1)\n", + "Color6 = Theory(\"Color6\", relation_name=\"C6\", arity=1)\n", + "Color7 = Theory(\"Color7\", relation_name=\"C7\", arity=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "2835afb0-55c7-44d6-9078-cd337c2ae629", + "metadata": {}, + "outputs": [], + "source": [ + "DiGraphTheory.exclude(DiGraphTheory(2, edges=[[0, 1], [1, 0]]))" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "4d2d8a1d-3292-43f0-b76b-5e7ea30ac9de", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(Flag on 3 points, ftype from () with edges3=(), Flag on 3 points, ftype from () with edges3=(012))\n" + ] + }, + { + "data": { + "text/plain": [ + "Flag on 4 points, ftype from () with edges3=(012 023 123)" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "###\n", + "### Creating new theories\n", + "###\n", + "\n", + "# One can create a new theory using the command:\n", + "# Theory([\"name_for_the_theory\"], relation_name=[\"name_for_the_relation\"], arity=[arity_of_relation], is_ordered=[is_relation_ordered])\n", + "\n", + "# The default values are: relation_name=\"edges\", arity=2, is_orderd=False\n", + "# Here are a few examples\n", + "\n", + "OtherDiGraphTheory = Theory(\"OtherDiGraph\", arity=2, is_ordered=True, relation_name=\"diedge\")\n", + "OtherThreeGraphTheory = Theory(\"OtherThreeGraph\", arity=3, relation_name=\"edges3\")\n", + "\n", + "# The elements in the new theory has relations named accordingly\n", + "print(OtherThreeGraphTheory.generate(3))\n", + "\n", + "# And creating elements must also follow the convention\n", + "k4m = OtherThreeGraphTheory(4, edges3=[[0, 1, 2], [1, 2, 3], [0, 2, 3]])\n", + "k4m" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "8849dbbf-161a-42c2-9095-76c019e1141f", + "metadata": {}, + "outputs": [], + "source": [ + "###\n", + "### Excluding things\n", + "###\n", + "\n", + "# To reset a theory, so nothing is excluded, call\n", + "G.reset()\n", + "\n", + "# A list of flags can be excluded\n", + "structure_1 = G(4, edges=[[0, 1], [0, 2], [0, 3], [1, 2]])\n", + "structure_2 = G(3, edges=[[0, 1], [0, 2]])\n", + "G.exclude([structure_1, structure_2])\n", + "\n", + "# Exclude works incrementally, the following works too\n", + "G.reset()\n", + "G.exclude(structure_1)\n", + "G.exclude(structure_2)\n", + "\n", + "# Excluding also allows patterns, \n", + "G.reset()\n", + "G.exclude(G.p(4, edges=[[0, 1], [1, 2], [2, 3], [0, 2]]))" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "384b5ad6-e856-46b3-a6f4-0d5308f62eb4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(Flag on 4 points, ftype from () with edges=(),\n", + " Flag on 4 points, ftype from () with edges=(01),\n", + " Flag on 4 points, ftype from () with edges=(01 03),\n", + " Flag on 4 points, ftype from () with edges=(02 13),\n", + " Flag on 4 points, ftype from () with edges=(01 02 03),\n", + " Flag on 4 points, ftype from () with edges=(01 03 13),\n", + " Flag on 4 points, ftype from () with edges=(01 02 13),\n", + " Flag on 4 points, ftype from () with edges=(02 03 12 13))" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "###\n", + "### Generating flags\n", + "###\n", + "\n", + "# To generate all flags (respecting the excluded structures, see the above cell)\n", + "G.generate(4)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "4ca23a2c-7578-458d-bdb6-ccfbfe875d8d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(Flag on 4 points, ftype from (0, 1) with edges=(01),\n", + " Flag on 4 points, ftype from (0, 1) with edges=(01 03),\n", + " Flag on 4 points, ftype from (1, 0) with edges=(01 03),\n", + " Flag on 4 points, ftype from (0, 2) with edges=(02 13),\n", + " Flag on 4 points, ftype from (0, 1) with edges=(01 02 03),\n", + " Flag on 4 points, ftype from (1, 0) with edges=(01 02 03),\n", + " Flag on 4 points, ftype from (0, 1) with edges=(01 03 13),\n", + " Flag on 4 points, ftype from (0, 1) with edges=(01 02 13),\n", + " Flag on 4 points, ftype from (0, 2) with edges=(01 02 13),\n", + " Flag on 4 points, ftype from (2, 0) with edges=(01 02 13),\n", + " Flag on 4 points, ftype from (0, 1) with edges=(01 02 03 13),\n", + " Flag on 4 points, ftype from (0, 2) with edges=(01 02 03 13),\n", + " Flag on 4 points, ftype from (1, 0) with edges=(01 02 03 13),\n", + " Flag on 4 points, ftype from (1, 3) with edges=(01 02 03 13),\n", + " Flag on 4 points, ftype from (2, 0) with edges=(01 02 03 13),\n", + " Flag on 4 points, ftype from (0, 2) with edges=(02 03 12 13),\n", + " Flag on 4 points, ftype from (0, 1) with edges=(01 02 03 12 13),\n", + " Flag on 4 points, ftype from (0, 2) with edges=(01 02 03 12 13),\n", + " Flag on 4 points, ftype from (2, 0) with edges=(01 02 03 12 13),\n", + " Flag on 4 points, ftype from (0, 1) with edges=(01 02 03 12 13 23))" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# It is also possible to generate all flags with a given type\n", + "edgetype = G(2, edges=[[0, 1]], ftype=[0, 1])\n", + "G.reset()\n", + "G.generate(4, edgetype)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "7a766765-ab70-49a0-b466-84e595c35d10", + "metadata": {}, + "outputs": [ + { + "ename": "ValueError", + "evalue": "The relation names must be different!", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[23], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m#this gives error, both uses the edges relation name\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[43mcombine\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mCombinedThing\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mGraphTheory\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mThreeGraphTheory\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/sage/src/sage/algebras/combinatorial_theory.py:3957\u001b[0m, in \u001b[0;36mcombine\u001b[0;34m(name, symmetries, *theories)\u001b[0m\n\u001b[1;32m 3955\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m kk \u001b[38;5;129;01min\u001b[39;00m theory\u001b[38;5;241m.\u001b[39m_signature:\n\u001b[1;32m 3956\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m kk \u001b[38;5;129;01min\u001b[39;00m result_signature:\n\u001b[0;32m-> 3957\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mThe relation names must be different!\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 3958\u001b[0m tkk \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mdict\u001b[39m(theory\u001b[38;5;241m.\u001b[39m_signature[kk])\n\u001b[1;32m 3959\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m can_symmetry:\n", + "\u001b[0;31mValueError\u001b[0m: The relation names must be different!" + ] + } + ], + "source": [ + "#this gives error, both uses the edges relation name\n", + "combine(\"CombinedThing\", GraphTheory, ThreeGraphTheory)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "011bc895-a8e7-4be4-a792-914e91752de9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(Flag on 3 points, ftype from () with edges=(01), edges3=(),\n", + " Flag on 3 points, ftype from () with edges=(01), edges3=(012),\n", + " Flag on 3 points, ftype from () with edges=(01 02), edges3=(),\n", + " Flag on 3 points, ftype from () with edges=(01 02), edges3=(012),\n", + " Flag on 3 points, ftype from () with edges=(01 02 12), edges3=(),\n", + " Flag on 3 points, ftype from () with edges=(01 02 12), edges3=(012))" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# To create complex theories, it is possible to combine them.\n", + "\n", + "# For this to work, the theories must have different relation_name-s\n", + "\n", + "# We can't combine GraphTheory and ThreeGraphTheory since they both use the \"edges\" relation\n", + "# So let's create an alternative ThreeGraphTheory using edges3 relation\n", + "\n", + "TG = Theory(\"OtherThreeGraph\", arity=3, relation_name=\"edges3\")\n", + "\n", + "# Then we can combine the theories\n", + "# First a name is needed, then the list of theories\n", + "G.reset()\n", + "G.exclude(G(3))\n", + "CombinedTheory = combine(\"TwoThreeGraph\", G, TG)\n", + "CombinedTheory.reset()\n", + "CombinedTheory.generate(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "6063870c-f6f1-4ed0-b74d-531ff06ee65d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(Flag on 3 points, ftype from () with edges=(), edges3=(),\n", + " Flag on 3 points, ftype from () with edges=(), edges3=(012),\n", + " Flag on 3 points, ftype from () with edges=(01), edges3=(),\n", + " Flag on 3 points, ftype from () with edges=(01), edges3=(012),\n", + " Flag on 3 points, ftype from () with edges=(01 02), edges3=(),\n", + " Flag on 3 points, ftype from () with edges=(01 02), edges3=(012),\n", + " Flag on 3 points, ftype from () with edges=(01 02 12), edges3=(),\n", + " Flag on 3 points, ftype from () with edges=(01 02 12), edges3=(012))" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "G.reset()\n", + "CombinedTheory.generate(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "dac44a0a-d8e9-4547-ba7e-04cd4acf647e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(Flag on 4 points, ftype from () with edges=(), edges3=(),\n", + " Flag on 4 points, ftype from () with edges=(), edges3=(012),\n", + " Flag on 4 points, ftype from () with edges=(), edges3=(012 013),\n", + " Flag on 4 points, ftype from () with edges=(), edges3=(012 013 023),\n", + " Flag on 4 points, ftype from () with edges=(), edges3=(012 013 023 123),\n", + " Flag on 4 points, ftype from () with edges=(02), edges3=(),\n", + " Flag on 4 points, ftype from () with edges=(23), edges3=(012),\n", + " Flag on 4 points, ftype from () with edges=(23), edges3=(012 013),\n", + " Flag on 4 points, ftype from () with edges=(23), edges3=(012 013 023),\n", + " Flag on 4 points, ftype from () with edges=(02), edges3=(012 013 023 123),\n", + " Flag on 4 points, ftype from () with edges=(02 03), edges3=(),\n", + " Flag on 4 points, ftype from () with edges=(13 23), edges3=(012),\n", + " Flag on 4 points, ftype from () with edges=(13 23), edges3=(012 013 023),\n", + " Flag on 4 points, ftype from () with edges=(01 23), edges3=(),\n", + " Flag on 4 points, ftype from () with edges=(01 23), edges3=(012 013 023 123),\n", + " Flag on 4 points, ftype from () with edges=(03 13 23), edges3=(),\n", + " Flag on 4 points, ftype from () with edges=(03 13 23), edges3=(012),\n", + " Flag on 4 points, ftype from () with edges=(02 03 23), edges3=(),\n", + " Flag on 4 points, ftype from () with edges=(01 02 12), edges3=(012),\n", + " Flag on 4 points, ftype from () with edges=(12 13 23), edges3=(012 013 023),\n", + " Flag on 4 points, ftype from () with edges=(02 03 23), edges3=(012 013 023 123),\n", + " Flag on 4 points, ftype from () with edges=(01 02 23), edges3=(),\n", + " Flag on 4 points, ftype from () with edges=(01 02 03 23), edges3=(),\n", + " Flag on 4 points, ftype from () with edges=(01 02 12 23), edges3=(012),\n", + " Flag on 4 points, ftype from () with edges=(01 03 12 23), edges3=(),\n", + " Flag on 4 points, ftype from () with edges=(01 02 03 12 23), edges3=(),\n", + " Flag on 4 points, ftype from () with edges=(01 02 03 12 23), edges3=(012),\n", + " Flag on 4 points, ftype from () with edges=(01 02 03 12 13), edges3=(012 013),\n", + " Flag on 4 points, ftype from () with edges=(01 02 03 12 13 23), edges3=(),\n", + " Flag on 4 points, ftype from () with edges=(01 02 03 12 13 23), edges3=(012),\n", + " Flag on 4 points, ftype from () with edges=(01 02 03 12 13 23), edges3=(012 013),\n", + " Flag on 4 points, ftype from () with edges=(01 02 03 12 13 23), edges3=(012 013 023),\n", + " Flag on 4 points, ftype from () with edges=(01 02 03 12 13 23), edges3=(012 013 023 123))" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# For combined theories, we can create flags by specifying the relations separately\n", + "test_flag = CombinedTheory(3, edges=[[0, 1], [0, 2]], edges3=[[0, 1, 2]])\n", + "\n", + "# Patterns work too\n", + "test_pattern = CombinedTheory.p(4, edges=[[0, 1]], edges_m=[[1, 2]], edges3=[[0, 1, 2]], edges3_m=[[1, 2, 3]])\n", + "\n", + "# See what happens if we exclude these\n", + "CombinedTheory.exclude([test_flag, test_pattern])\n", + "CombinedTheory.generate(4)" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "ad5027ed-df99-4f6e-a035-04b49f54b727", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Without symmetry, we have the structures: \n", + "Flag on 2 points, ftype from () with edges=(), oedges=()\n", + "Flag on 2 points, ftype from () with edges=(), oedges=(01)\n", + "Flag on 2 points, ftype from () with edges=(01), oedges=()\n", + "Flag on 2 points, ftype from () with edges=(01), oedges=(01)\n", + "\n", + "\n", + "With symmetry, we have the structures: \n", + "Flag on 2 points, ftype from () with edges=(), oedges=()\n", + "Flag on 2 points, ftype from () with edges=(), oedges=(01)\n", + "Flag on 2 points, ftype from () with edges=(01), oedges=(01)\n" + ] + } + ], + "source": [ + "# If the relations combined have the same arity, all ordered or all unordered\n", + "# Then a symmetry can be specified\n", + "\n", + "Gp = Theory(\"OtherGraph\", relation_name=\"oedges\")\n", + "\n", + "# By default there is no symmetry. If we omit symmetries, then no symmetry is assumed\n", + "Test1 = combine(\"DoubleEdgeGraph\", G, Gp, symmetries=NoSymmetry)\n", + "Test2 = combine(\"SymmetricDoubleEdgeGraph\", G, Gp, symmetries=FullSymmetry)\n", + "\n", + "\n", + "print(\"Without symmetry, we have the structures: \")\n", + "print(\"\\n\".join(map(str, Test1.generate(2))))\n", + "print(\"\\n\\nWith symmetry, we have the structures: \")\n", + "print(\"\\n\".join(map(str, Test2.generate(2))))\n", + "# Note that as the edges and other edges are identical in the second case, \n", + "# the flag with only one of these present is included only once" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "fc7d27e3-35e5-446b-957f-cba155707eb2", + "metadata": {}, + "outputs": [], + "source": [ + "# For colors, there are pre-defined theories with different names.\n", + "# There are also pre-defined symmetry groups.\n", + "\n", + "Cyclic5Colors = combine(\"Cyclic5Colors\", Color0, Color1, Color2, Color3, Color4, symmetries=CyclicSymmetry(5))\n", + "Cyclic5Colors.exclude([\n", + " Cyclic5Colors(1), \n", + " Cyclic5Colors.p(1, C0=[0], C1=[0]),\n", + " Cyclic5Colors.p(1, C0=[0], C2=[0])\n", + "])\n", + "\n", + "\n", + "# There is also a symmetry for the k4m colors\n", + "NoK4mColors = combine(\"NoK4mColors\", Color0, Color1, Color2, Color3, Color4, Color5, symmetries=K4mSymmetry)\n", + "\n", + "# Then, these can be combined with other theories.\n", + "TT = combine(\"Cyclic5ColoredGraphs\", G, Cyclic5Colors)" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "4c801149-4402-42c7-8c55-7e4fcf51d073", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(Flag on 3 points, ftype from () with edges=(), C0=(), C1=(), C2=(), C3=(), C4=(0 1 2),\n", + " Flag on 3 points, ftype from () with edges=(), C0=(), C1=(), C2=(), C3=(0), C4=(1 2),\n", + " Flag on 3 points, ftype from () with edges=(), C0=(), C1=(), C2=(0), C3=(), C4=(1 2),\n", + " Flag on 3 points, ftype from () with edges=(), C0=(), C1=(0), C2=(), C3=(), C4=(1 2),\n", + " Flag on 3 points, ftype from () with edges=(), C0=(0), C1=(), C2=(), C3=(), C4=(1 2),\n", + " Flag on 3 points, ftype from () with edges=(), C0=(), C1=(), C2=(0), C3=(1), C4=(2),\n", + " Flag on 3 points, ftype from () with edges=(), C0=(), C1=(0), C2=(), C3=(1), C4=(2),\n", + " Flag on 3 points, ftype from () with edges=(01), C0=(), C1=(), C2=(), C3=(), C4=(0 1 2),\n", + " Flag on 3 points, ftype from () with edges=(01), C0=(), C1=(), C2=(), C3=(0), C4=(1 2),\n", + " Flag on 3 points, ftype from () with edges=(12), C0=(), C1=(), C2=(), C3=(0), C4=(1 2),\n", + " Flag on 3 points, ftype from () with edges=(01), C0=(), C1=(), C2=(0), C3=(), C4=(1 2),\n", + " Flag on 3 points, ftype from () with edges=(12), C0=(), C1=(), C2=(0), C3=(), C4=(1 2),\n", + " Flag on 3 points, ftype from () with edges=(01), C0=(), C1=(0), C2=(), C3=(), C4=(1 2),\n", + " Flag on 3 points, ftype from () with edges=(12), C0=(), C1=(0), C2=(), C3=(), C4=(1 2),\n", + " Flag on 3 points, ftype from () with edges=(01), C0=(0), C1=(), C2=(), C3=(), C4=(1 2),\n", + " Flag on 3 points, ftype from () with edges=(12), C0=(0), C1=(), C2=(), C3=(), C4=(1 2),\n", + " Flag on 3 points, ftype from () with edges=(01), C0=(), C1=(), C2=(0), C3=(1), C4=(2),\n", + " Flag on 3 points, ftype from () with edges=(02), C0=(), C1=(), C2=(0), C3=(1), C4=(2),\n", + " Flag on 3 points, ftype from () with edges=(12), C0=(), C1=(), C2=(0), C3=(1), C4=(2),\n", + " Flag on 3 points, ftype from () with edges=(01), C0=(), C1=(0), C2=(), C3=(1), C4=(2),\n", + " Flag on 3 points, ftype from () with edges=(02), C0=(), C1=(0), C2=(), C3=(1), C4=(2),\n", + " Flag on 3 points, ftype from () with edges=(12), C0=(), C1=(0), C2=(), C3=(1), C4=(2),\n", + " Flag on 3 points, ftype from () with edges=(01 02), C0=(), C1=(), C2=(), C3=(), C4=(0 1 2),\n", + " Flag on 3 points, ftype from () with edges=(01 02), C0=(), C1=(), C2=(), C3=(0), C4=(1 2),\n", + " Flag on 3 points, ftype from () with edges=(01 12), C0=(), C1=(), C2=(), C3=(0), C4=(1 2),\n", + " Flag on 3 points, ftype from () with edges=(01 02), C0=(), C1=(), C2=(0), C3=(), C4=(1 2),\n", + " Flag on 3 points, ftype from () with edges=(01 12), C0=(), C1=(), C2=(0), C3=(), C4=(1 2),\n", + " Flag on 3 points, ftype from () with edges=(01 02), C0=(), C1=(0), C2=(), C3=(), C4=(1 2),\n", + " Flag on 3 points, ftype from () with edges=(01 12), C0=(), C1=(0), C2=(), C3=(), C4=(1 2),\n", + " Flag on 3 points, ftype from () with edges=(01 02), C0=(0), C1=(), C2=(), C3=(), C4=(1 2),\n", + " Flag on 3 points, ftype from () with edges=(01 12), C0=(0), C1=(), C2=(), C3=(), C4=(1 2),\n", + " Flag on 3 points, ftype from () with edges=(01 02), C0=(), C1=(), C2=(0), C3=(1), C4=(2),\n", + " Flag on 3 points, ftype from () with edges=(01 12), C0=(), C1=(), C2=(0), C3=(1), C4=(2),\n", + " Flag on 3 points, ftype from () with edges=(02 12), C0=(), C1=(), C2=(0), C3=(1), C4=(2),\n", + " Flag on 3 points, ftype from () with edges=(01 02), C0=(), C1=(0), C2=(), C3=(1), C4=(2),\n", + " Flag on 3 points, ftype from () with edges=(01 12), C0=(), C1=(0), C2=(), C3=(1), C4=(2),\n", + " Flag on 3 points, ftype from () with edges=(02 12), C0=(), C1=(0), C2=(), C3=(1), C4=(2),\n", + " Flag on 3 points, ftype from () with edges=(01 02 12), C0=(), C1=(), C2=(), C3=(), C4=(0 1 2),\n", + " Flag on 3 points, ftype from () with edges=(01 02 12), C0=(), C1=(), C2=(), C3=(0), C4=(1 2),\n", + " Flag on 3 points, ftype from () with edges=(01 02 12), C0=(), C1=(), C2=(0), C3=(), C4=(1 2),\n", + " Flag on 3 points, ftype from () with edges=(01 02 12), C0=(), C1=(0), C2=(), C3=(), C4=(1 2),\n", + " Flag on 3 points, ftype from () with edges=(01 02 12), C0=(0), C1=(), C2=(), C3=(), C4=(1 2),\n", + " Flag on 3 points, ftype from () with edges=(01 02 12), C0=(), C1=(), C2=(0), C3=(1), C4=(2),\n", + " Flag on 3 points, ftype from () with edges=(01 02 12), C0=(), C1=(0), C2=(), C3=(1), C4=(2))" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "TT.generate(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "1d6374cf-638a-470a-8f7f-77531ae48fdb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(Flag on 3 points, ftype from () with C0=(), C1=(), C2=(0 1 2), edges=(01),\n", + " Flag on 3 points, ftype from () with C0=(), C1=(), C2=(0 1 2), edges=(01 02),\n", + " Flag on 3 points, ftype from () with C0=(), C1=(), C2=(0 1 2), edges=(01 02 12),\n", + " Flag on 3 points, ftype from () with C0=(), C1=(0), C2=(1 2), edges=(01),\n", + " Flag on 3 points, ftype from () with C0=(), C1=(2), C2=(0 1), edges=(01),\n", + " Flag on 3 points, ftype from () with C0=(), C1=(0), C2=(1 2), edges=(01 02),\n", + " Flag on 3 points, ftype from () with C0=(), C1=(1), C2=(0 2), edges=(01 02),\n", + " Flag on 3 points, ftype from () with C0=(), C1=(0), C2=(1 2), edges=(01 02 12),\n", + " Flag on 3 points, ftype from () with C0=(0), C1=(), C2=(1 2), edges=(01),\n", + " Flag on 3 points, ftype from () with C0=(2), C1=(), C2=(0 1), edges=(01),\n", + " Flag on 3 points, ftype from () with C0=(0), C1=(), C2=(1 2), edges=(01 02),\n", + " Flag on 3 points, ftype from () with C0=(1), C1=(), C2=(0 2), edges=(01 02),\n", + " Flag on 3 points, ftype from () with C0=(0), C1=(), C2=(1 2), edges=(01 02 12),\n", + " Flag on 3 points, ftype from () with C0=(0), C1=(1), C2=(2), edges=(01),\n", + " Flag on 3 points, ftype from () with C0=(0), C1=(1), C2=(2), edges=(01 02),\n", + " Flag on 3 points, ftype from () with C0=(0), C1=(1), C2=(2), edges=(01 02 12))" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Altough the exclusion can happen in the combined theories, \n", + "# it is also possible to exclude before combining. \n", + "# In that case, the exclusions at the moment they are combined are used.\n", + "\n", + "# Here is an example where Colors are combined, but they are forced to be disjoint first\n", + "Cyclic3Colors = combine(\"Cyclic3Colors\", Color0, Color1, Color2, symmetries=CyclicSymmetry(3))\n", + "# This guarantees the colors are disjoint (due to the symmetry)\n", + "Cyclic3Colors.exclude([\n", + " Cyclic3Colors(1), \n", + " Cyclic3Colors.p(1, C0=[0], C1=[0])\n", + "])\n", + "\n", + "# Make graphs without empty triples\n", + "G.exclude(G(3))\n", + "\n", + "Cyclic3ColGraph = combine(\"Cyclic3Graph\", Cyclic3Colors, G)\n", + "# So the resulting theory uses disjoint colors and has no empty graphs\n", + "Cyclic3ColGraph.generate(3)\n", + "\n", + "# Note the changing the excluded structures in a component will change the\n", + "# excluded structures in the result too, even after the combination.\n", + "\n", + "# Running the below would give a different result as above.\n", + "# G.reset()\n", + "# Cyclic3ColGraph.generate(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "098b34ff-6b53-44a9-b57f-3bbd86d8ced0", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "Graphics object consisting of 47 graphics primitives" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Graph(K4mSymmetry).plot()" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "ccd5c66e-aedf-4433-882b-38978fa2cd35", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Theory for Cyclic3ColorsOther" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# To specify any symmetry, we can provide a symmetry graph\n", + "\n", + "# For this example, I will just recreate the cyclic 3 symmetry.\n", + "# To provide cyclic 3 symmetry, we need a graph, where supposing that the first 3 vertices\n", + "# are mapped to the first 3 vertices, the automorphism group of those 3 vertices is C3\n", + "\n", + "# The graph with the following edge set works:\n", + "Cyclic3 = [[0, 1], [0, 3],[0, 6],\n", + " [3, 7], [0, 7], [1, 2],\n", + " [1, 4], [1, 7], [4, 8],\n", + " [1, 8], [2, 0], [2, 5],\n", + " [2, 8], [5, 6], [2, 6]]\n", + "\n", + "# Therefore it is possible to use it in later inputs\n", + "combine(\"Cyclic3ColorsOther\", Color0, Color1, Color2, symmetries=Cyclic3)" + ] + }, + { + "cell_type": "markdown", + "id": "b1176d4e-4a1a-4510-a2a1-03957264571a", + "metadata": {}, + "source": [ + "Optimizing\n", + "==========\n", + "1. Assumptions\n", + "2. Rounding\n", + "3. Construction\n", + "4. Certificates\n", + "6. Exporting the SDP problem\n", + "8. Rounding over field extensions" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "cb3115c7-16f4-45af-8f1b-a9e7a3b2884d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Base flags generated, their number is 3\n", + "The relevant ftypes are constructed, their number is 1\n", + "Block sizes before symmetric/asymmetric change is applied: [2]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with mult table for Ftype on 1 points with edges=(): : 1it [00:00, 345.81it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tables finished\n", + "Constraints finished\n", + "Running sdp without construction. Used block sizes are [2, -3, -2]\n", + "CSDP 6.2.0\n", + "Iter: 0 Ap: 0.00e+00 Pobj: 0.0000000e+00 Ad: 0.00e+00 Dobj: 0.0000000e+00 \n", + "Iter: 1 Ap: 1.00e+00 Pobj: -1.5751371e+01 Ad: 7.93e-01 Dobj: -1.8596991e-01 \n", + "Iter: 2 Ap: 1.00e+00 Pobj: -1.3829292e+01 Ad: 9.44e-01 Dobj: -2.5639377e-01 \n", + "Iter: 3 Ap: 1.00e+00 Pobj: -3.6548137e+00 Ad: 9.22e-01 Dobj: -2.8097573e-01 \n", + "Iter: 4 Ap: 1.00e+00 Pobj: -6.7094195e-01 Ad: 8.42e-01 Dobj: -3.1080901e-01 \n", + "Iter: 5 Ap: 1.00e+00 Pobj: -5.8576089e-01 Ad: 8.47e-01 Dobj: -4.8072710e-01 \n", + "Iter: 6 Ap: 1.00e+00 Pobj: -5.0662032e-01 Ad: 8.78e-01 Dobj: -4.9275677e-01 \n", + "Iter: 7 Ap: 1.00e+00 Pobj: -5.0069959e-01 Ad: 9.33e-01 Dobj: -4.9911657e-01 \n", + "Iter: 8 Ap: 1.00e+00 Pobj: -5.0005821e-01 Ad: 1.00e+00 Dobj: -4.9996202e-01 \n", + "Iter: 9 Ap: 1.00e+00 Pobj: -5.0000265e-01 Ad: 1.00e+00 Dobj: -4.9999892e-01 \n", + "Iter: 10 Ap: 1.00e+00 Pobj: -5.0000020e-01 Ad: 1.00e+00 Dobj: -4.9999999e-01 \n", + "Iter: 11 Ap: 1.00e+00 Pobj: -5.0000001e-01 Ad: 9.90e-01 Dobj: -5.0000000e-01 \n", + "Iter: 12 Ap: 9.57e-01 Pobj: -5.0000000e-01 Ad: 9.69e-01 Dobj: -5.0000000e-01 \n", + "Success: SDP solved\n", + "Primal objective value: -5.0000000e-01 \n", + "Dual objective value: -5.0000000e-01 \n", + "Relative primal infeasibility: 1.13e-15 \n", + "Relative dual infeasibility: 1.18e-10 \n", + "Real Relative Gap: 2.59e-10 \n", + "XZ Relative Gap: 4.63e-10 \n", + "DIMACS error measures: 1.18e-15 0.00e+00 2.46e-10 0.00e+00 2.59e-10 4.63e-10\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/plain": [ + "0.5000000004275388" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# To run the optimizer, we can call the .optimize function on a theory\n", + "\n", + "G.reset()\n", + "G.exclude(triangle)\n", + "\n", + "# The basic requirement is to have a target we optimize for, and a target size\n", + "# By default it maximizes, here edges and target size is 3\n", + "G.optimize(edge, 3)" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "a59aae0d-79c6-48df-a23d-480f82efea2f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Base flags generated, their number is 7\n", + "The relevant ftypes are constructed, their number is 2\n", + "Block sizes before symmetric/asymmetric change is applied: [4, 3]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with mult table for Ftype on 2 points with edges=(01): : 2it [00:00, 7.93it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tables finished\n", + "Constraints finished\n", + "Running sdp without construction. Used block sizes are [3, 1, 2, 1, -7, -2]\n", + "CSDP 6.2.0\n", + "Iter: 0 Ap: 0.00e+00 Pobj: 0.0000000e+00 Ad: 0.00e+00 Dobj: 0.0000000e+00 \n", + "Iter: 1 Ap: 1.00e+00 Pobj: -1.8806565e+01 Ad: 7.54e-01 Dobj: -6.9981849e-02 \n", + "Iter: 2 Ap: 1.00e+00 Pobj: -1.8408806e+01 Ad: 9.44e-01 Dobj: -5.3423421e-01 \n", + "Iter: 3 Ap: 1.00e+00 Pobj: -8.9956342e+00 Ad: 8.73e-01 Dobj: -5.3133258e-01 \n", + "Iter: 4 Ap: 1.00e+00 Pobj: -2.7394355e+00 Ad: 7.87e-01 Dobj: -5.3695578e-01 \n", + "Iter: 5 Ap: 1.00e+00 Pobj: -9.4703414e-01 Ad: 8.73e-01 Dobj: -5.5895310e-01 \n", + "Iter: 6 Ap: 1.00e+00 Pobj: -7.4536200e-01 Ad: 8.81e-01 Dobj: -6.3958028e-01 \n", + "Iter: 7 Ap: 1.00e+00 Pobj: -6.7883373e-01 Ad: 8.58e-01 Dobj: -6.5988123e-01 \n", + "Iter: 8 Ap: 1.00e+00 Pobj: -6.6776858e-01 Ad: 9.34e-01 Dobj: -6.6583853e-01 \n", + "Iter: 9 Ap: 1.00e+00 Pobj: -6.6674852e-01 Ad: 9.94e-01 Dobj: -6.6663187e-01 \n", + "Iter: 10 Ap: 9.81e-01 Pobj: -6.6667197e-01 Ad: 1.00e+00 Dobj: -6.6666778e-01 \n", + "Iter: 11 Ap: 1.00e+00 Pobj: -6.6666696e-01 Ad: 1.00e+00 Dobj: -6.6666691e-01 \n", + "Iter: 12 Ap: 1.00e+00 Pobj: -6.6666668e-01 Ad: 9.79e-01 Dobj: -6.6666667e-01 \n", + "Iter: 13 Ap: 9.59e-01 Pobj: -6.6666667e-01 Ad: 9.59e-01 Dobj: -6.6666667e-01 \n", + "Success: SDP solved\n", + "Primal objective value: -6.6666667e-01 \n", + "Dual objective value: -6.6666667e-01 \n", + "Relative primal infeasibility: 7.41e-14 \n", + "Relative dual infeasibility: 1.52e-10 \n", + "Real Relative Gap: 1.08e-10 \n", + "XZ Relative Gap: 5.12e-10 \n", + "DIMACS error measures: 1.00e-13 0.00e+00 4.23e-10 0.00e+00 1.08e-10 5.12e-10\n" + ] + }, + { + "data": { + "text/plain": [ + "0.6666666672726256" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# The target function can be any liner combination of flags\n", + "G.optimize(edge + G(3, edges=[[0, 1]]), 4)" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "90f162ca-a0f0-4d5e-91e5-af5ff1bb00d4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Base flags generated, their number is 11\n", + "The relevant ftypes are constructed, their number is 2\n", + "Block sizes before symmetric/asymmetric change is applied: [4, 4]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with mult table for Ftype on 2 points with edges=(01): : 2it [00:00, 564.81it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tables finished\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with positivity constraint 0: 100%|█████████| 1/1 [00:00<00:00, 189.27it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Constraints finished\n", + "Running sdp without construction. Used block sizes are [3, 1, 3, 1, -11, -4]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CSDP 6.2.0\n", + "Iter: 0 Ap: 0.00e+00 Pobj: 0.0000000e+00 Ad: 0.00e+00 Dobj: 0.0000000e+00 \n", + "Iter: 1 Ap: 1.00e+00 Pobj: -2.0362520e+01 Ad: 7.29e-01 Dobj: -6.0813905e-02 \n", + "Iter: 2 Ap: 1.00e+00 Pobj: -2.0324044e+01 Ad: 9.49e-01 Dobj: -1.4090950e-01 \n", + "Iter: 3 Ap: 1.00e+00 Pobj: -1.2792797e+01 Ad: 8.87e-01 Dobj: -1.4525769e-01 \n", + "Iter: 4 Ap: 1.00e+00 Pobj: -2.6397236e+00 Ad: 8.16e-01 Dobj: -1.4716863e-01 \n", + "Iter: 5 Ap: 9.70e-01 Pobj: -5.7645242e-01 Ad: 8.89e-01 Dobj: -1.6649882e-01 \n", + "Iter: 6 Ap: 1.00e+00 Pobj: -4.7413525e-01 Ad: 8.51e-01 Dobj: -2.9000569e-01 \n", + "Iter: 7 Ap: 1.00e+00 Pobj: -3.7538701e-01 Ad: 7.49e-01 Dobj: -3.2184161e-01 \n", + "Iter: 8 Ap: 1.00e+00 Pobj: -3.5709889e-01 Ad: 8.67e-01 Dobj: -3.4598987e-01 \n", + "Iter: 9 Ap: 1.00e+00 Pobj: -3.5385657e-01 Ad: 1.00e+00 Dobj: -3.5309874e-01 \n", + "Iter: 10 Ap: 9.99e-01 Pobj: -3.5356572e-01 Ad: 1.00e+00 Dobj: -3.5353550e-01 \n", + "Iter: 11 Ap: 9.94e-01 Pobj: -3.5355424e-01 Ad: 1.00e+00 Dobj: -3.5355362e-01 \n", + "Iter: 12 Ap: 1.00e+00 Pobj: -3.5355343e-01 Ad: 9.87e-01 Dobj: -3.5355340e-01 \n", + "Iter: 13 Ap: 9.59e-01 Pobj: -3.5355339e-01 Ad: 9.57e-01 Dobj: -3.5355339e-01 \n", + "Success: SDP solved\n", + "Primal objective value: -3.5355339e-01 \n", + "Dual objective value: -3.5355339e-01 \n", + "Relative primal infeasibility: 1.82e-15 \n", + "Relative dual infeasibility: 1.84e-09 \n", + "Real Relative Gap: 3.80e-10 \n", + "XZ Relative Gap: 3.51e-09 \n", + "DIMACS error measures: 1.98e-15 0.00e+00 4.99e-09 0.00e+00 3.80e-10 3.51e-09\n" + ] + }, + { + "data": { + "text/plain": [ + "0.3535533923547919" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "###\n", + "### Assumptions\n", + "###\n", + "\n", + "# It is possible to add assumptions to the optimizer. \n", + "# Any linear combination of flags included will be assumed to be non-negative\n", + "\n", + "G.reset()\n", + "# Here positives=[ 1/2 - edge] asserts that 1/2-edge >= 0, so the density of edges is at most 1/2\n", + "# We maximize the number of triangles under this assumption\n", + "G.optimize(triangle, 4, positives=[ 1/2 - edge ])" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "41b4b0a6-e34f-4472-bde4-b6d389582559", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Base flags generated, their number is 11\n", + "The relevant ftypes are constructed, their number is 2\n", + "Block sizes before symmetric/asymmetric change is applied: [4, 4]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with mult table for Ftype on 2 points with edges=(01): : 2it [00:00, 719.74it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tables finished\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with positivity constraint 0: 100%|██████████| 1/1 [00:00<00:00, 4.70it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Constraints finished\n", + "Running sdp without construction. Used block sizes are [3, 1, 3, 1, -11, -8]\n", + "CSDP 6.2.0\n", + "Iter: 0 Ap: 0.00e+00 Pobj: 0.0000000e+00 Ad: 0.00e+00 Dobj: 0.0000000e+00 \n", + "Iter: 1 Ap: 1.00e+00 Pobj: -2.0351247e+01 Ad: 7.28e-01 Dobj: 1.9241068e-01 \n", + "Iter: 2 Ap: 1.00e+00 Pobj: -2.0298857e+01 Ad: 9.49e-01 Dobj: -1.1331216e-01 \n", + "Iter: 3 Ap: 1.00e+00 Pobj: -1.2166750e+01 Ad: 8.89e-01 Dobj: -1.1297865e-01 \n", + "Iter: 4 Ap: 9.44e-01 Pobj: -3.5639778e+00 Ad: 7.81e-01 Dobj: -8.8235703e-02 \n", + "Iter: 5 Ap: 1.00e+00 Pobj: -1.0447222e+00 Ad: 8.33e-01 Dobj: -8.4380440e-02 \n", + "Iter: 6 Ap: 9.75e-01 Pobj: -3.1581467e-01 Ad: 8.64e-01 Dobj: -1.0555082e-01 \n", + "Iter: 7 Ap: 1.00e+00 Pobj: -3.0596168e-01 Ad: 7.93e-01 Dobj: -2.0824259e-01 \n", + "Iter: 8 Ap: 1.00e+00 Pobj: -2.5581926e-01 Ad: 8.13e-01 Dobj: -2.3309729e-01 \n", + "Iter: 9 Ap: 1.00e+00 Pobj: -2.5041006e-01 Ad: 9.80e-01 Dobj: -2.4855614e-01 \n", + "Iter: 10 Ap: 1.00e+00 Pobj: -2.5001867e-01 Ad: 1.00e+00 Dobj: -2.4993040e-01 \n", + "Iter: 11 Ap: 1.00e+00 Pobj: -2.5000179e-01 Ad: 9.88e-01 Dobj: -2.4999529e-01 \n", + "Iter: 12 Ap: 1.00e+00 Pobj: -2.5000054e-01 Ad: 1.00e+00 Dobj: -2.4999886e-01 \n", + "Iter: 13 Ap: 1.00e+00 Pobj: -2.5000007e-01 Ad: 1.00e+00 Dobj: -2.4999989e-01 \n", + "Iter: 14 Ap: 1.00e+00 Pobj: -2.5000000e-01 Ad: 1.00e+00 Dobj: -2.4999999e-01 \n", + "Iter: 15 Ap: 9.60e-01 Pobj: -2.5000000e-01 Ad: 9.54e-01 Dobj: -2.5000000e-01 \n", + "Success: SDP solved\n", + "Primal objective value: -2.5000000e-01 \n", + "Dual objective value: -2.5000000e-01 \n", + "Relative primal infeasibility: 1.34e-14 \n", + "Relative dual infeasibility: 1.46e-10 \n", + "Real Relative Gap: 3.78e-10 \n", + "XZ Relative Gap: 7.90e-10 \n", + "DIMACS error measures: 1.46e-14 0.00e+00 3.95e-10 0.00e+00 3.78e-10 7.90e-10\n" + ] + }, + { + "data": { + "text/plain": [ + "0.2500000001939642" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Can also add positive constraints involving types. Then this is assumed to hold for all type\n", + "\n", + "# The same optimization, but now we assume every vertex has relative degree at most 1/2\n", + "G.optimize_problem(G(3), 4, positives=[ 1/2 - G(2, ftype=[0]) ])" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "78d5c2bf-cf3d-4264-a155-6a797eeb45bc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Base flags generated, their number is 11\n", + "The relevant ftypes are constructed, their number is 2\n", + "Block sizes before symmetric/asymmetric change is applied: [4, 4]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with mult table for Ftype on 2 points with edges=(01): : 2it [00:00, 851.46it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tables finished\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with positivity constraint 1: 100%|██████████| 2/2 [00:00<00:00, 18.16it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Constraints finished\n", + "Running sdp without construction. Used block sizes are [3, 1, 3, 1, -11, -4]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CSDP 6.2.0\n", + "Iter: 0 Ap: 0.00e+00 Pobj: 0.0000000e+00 Ad: 0.00e+00 Dobj: 0.0000000e+00 \n", + "Iter: 1 Ap: 1.00e+00 Pobj: -1.8017644e+01 Ad: 7.48e-01 Dobj: 3.1540360e+00 \n", + "Iter: 2 Ap: 1.00e+00 Pobj: -1.7888131e+01 Ad: 9.47e-01 Dobj: 6.8888413e-01 \n", + "Iter: 3 Ap: 1.00e+00 Pobj: -1.0366074e+01 Ad: 8.82e-01 Dobj: 6.3185654e-01 \n", + "Iter: 4 Ap: 1.00e+00 Pobj: -2.2050229e+00 Ad: 7.85e-01 Dobj: 6.4393155e-01 \n", + "Iter: 5 Ap: 1.00e+00 Pobj: 2.5269727e-01 Ad: 8.86e-01 Dobj: 6.3929369e-01 \n", + "Iter: 6 Ap: 1.00e+00 Pobj: 4.8301428e-01 Ad: 8.90e-01 Dobj: 6.1016973e-01 \n", + "Iter: 7 Ap: 1.00e+00 Pobj: 5.2082258e-01 Ad: 7.25e-01 Dobj: 5.7817993e-01 \n", + "Iter: 8 Ap: 1.00e+00 Pobj: 5.5020117e-01 Ad: 8.34e-01 Dobj: 5.6249091e-01 \n", + "Iter: 9 Ap: 1.00e+00 Pobj: 5.5504209e-01 Ad: 1.00e+00 Dobj: 5.5604574e-01 \n", + "Iter: 10 Ap: 9.94e-01 Pobj: 5.5553248e-01 Ad: 1.00e+00 Dobj: 5.5557653e-01 \n", + "Iter: 11 Ap: 9.91e-01 Pobj: 5.5555401e-01 Ad: 1.00e+00 Dobj: 5.5555607e-01 \n", + "Iter: 12 Ap: 1.00e+00 Pobj: 5.5555548e-01 Ad: 9.93e-01 Dobj: 5.5555558e-01 \n", + "Iter: 13 Ap: 9.59e-01 Pobj: 5.5555555e-01 Ad: 9.58e-01 Dobj: 5.5555556e-01 \n", + "Success: SDP solved\n", + "Primal objective value: 5.5555555e-01 \n", + "Dual objective value: 5.5555556e-01 \n", + "Relative primal infeasibility: 1.21e-14 \n", + "Relative dual infeasibility: 1.97e-09 \n", + "Real Relative Gap: 1.62e-09 \n", + "XZ Relative Gap: 3.64e-09 \n", + "DIMACS error measures: 1.75e-14 0.00e+00 5.35e-09 0.00e+00 1.62e-09 3.64e-09\n" + ] + }, + { + "data": { + "text/plain": [ + "0.5555555525754569" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Listing multiple positivity assumptions works too.\n", + "# Also we can specify maximize=False, for a minimization problem\n", + "\n", + "# Minimize induced no edges, such that induced empty triple is at least 1/3 and \n", + "# induced triple with one edge is at least 1/3\n", + "G.optimize(G(2), 4, positives=[ G(3) - 1/3, G(3, edges=[[0, 1]]) - 1/3 ], maximize=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "97b56bd7-71a8-4ab4-8613-d960ea14171f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Base flags generated, their number is 3\n", + "The relevant ftypes are constructed, their number is 1\n", + "Block sizes before symmetric/asymmetric change is applied: [2]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with mult table for Ftype on 1 points with edges=(): : 1it [00:00, 524.48it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tables finished\n", + "Constraints finished\n", + "Running sdp without construction. Used block sizes are [2, -3, -2]\n", + "CSDP 6.2.0\n", + "Iter: 0 Ap: 0.00e+00 Pobj: 0.0000000e+00 Ad: 0.00e+00 Dobj: 0.0000000e+00 \n", + "Iter: 1 Ap: 1.00e+00 Pobj: -1.5751371e+01 Ad: 7.93e-01 Dobj: -1.8596991e-01 \n", + "Iter: 2 Ap: 1.00e+00 Pobj: -1.3829292e+01 Ad: 9.44e-01 Dobj: -2.5639377e-01 \n", + "Iter: 3 Ap: 1.00e+00 Pobj: -3.6548137e+00 Ad: 9.22e-01 Dobj: -2.8097573e-01 \n", + "Iter: 4 Ap: 1.00e+00 Pobj: -6.7101882e-01 Ad: 8.42e-01 Dobj: -3.1080817e-01 \n", + "Iter: 5 Ap: 1.00e+00 Pobj: -5.8576221e-01 Ad: 8.44e-01 Dobj: -4.8002517e-01 \n", + "Iter: 6 Ap: 1.00e+00 Pobj: -5.0647693e-01 Ad: 8.85e-01 Dobj: -4.9296282e-01 \n", + "Iter: 7 Ap: 1.00e+00 Pobj: -5.0060021e-01 Ad: 9.41e-01 Dobj: -4.9923796e-01 \n", + "Iter: 8 Ap: 1.00e+00 Pobj: -5.0005080e-01 Ad: 1.00e+00 Dobj: -4.9996691e-01 \n", + "Iter: 9 Ap: 1.00e+00 Pobj: -5.0000234e-01 Ad: 1.00e+00 Dobj: -4.9999913e-01 \n", + "Iter: 10 Ap: 1.00e+00 Pobj: -5.0000016e-01 Ad: 1.00e+00 Dobj: -5.0000000e-01 \n", + "Iter: 11 Ap: 9.58e-01 Pobj: -5.0000001e-01 Ad: 9.70e-01 Dobj: -5.0000000e-01 \n", + "Success: SDP solved\n", + "Primal objective value: -5.0000001e-01 \n", + "Dual objective value: -5.0000000e-01 \n", + "Relative primal infeasibility: 5.33e-16 \n", + "Relative dual infeasibility: 3.98e-09 \n", + "Real Relative Gap: 2.49e-09 \n", + "XZ Relative Gap: 9.08e-09 \n", + "DIMACS error measures: 5.58e-16 0.00e+00 8.28e-09 0.00e+00 2.49e-09 9.08e-09\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The initial run gave an accurate looking construction\n", + "Rounded construction vector is: \n", + "Flag Algebra Element over Rational Field\n", + "3/4 - Flag on 3 points, ftype from () with edges=(01)\n", + "0 - Flag on 3 points, ftype from () with edges=(01 02)\n", + "1/4 - Flag on 3 points, ftype from () with edges=(01 02 12)\n", + "Adjusting table with kernels from construction\n", + "Running SDP after kernel correction. Used block sizes are [1, -3, -2]\n", + "CSDP 6.2.0\n", + "Iter: 0 Ap: 0.00e+00 Pobj: 0.0000000e+00 Ad: 0.00e+00 Dobj: 0.0000000e+00 \n", + "Iter: 1 Ap: 1.00e+00 Pobj: -1.2107440e+01 Ad: 8.40e-01 Dobj: 6.1913653e-01 \n", + "Iter: 2 Ap: 1.00e+00 Pobj: -1.0543280e+01 Ad: 9.38e-01 Dobj: -1.3876525e-01 \n", + "Iter: 3 Ap: 1.00e+00 Pobj: -2.4595774e+00 Ad: 9.42e-01 Dobj: -1.9392703e-01 \n", + "Iter: 4 Ap: 9.51e-01 Pobj: -6.1484578e-01 Ad: 8.50e-01 Dobj: -2.3934806e-01 \n", + "Iter: 5 Ap: 1.00e+00 Pobj: -6.4529870e-01 Ad: 7.42e-01 Dobj: -4.8596320e-01 \n", + "Iter: 6 Ap: 9.78e-01 Pobj: -5.0782840e-01 Ad: 8.89e-01 Dobj: -4.9454657e-01 \n", + "Iter: 7 Ap: 9.90e-01 Pobj: -5.0036064e-01 Ad: 9.65e-01 Dobj: -4.9953666e-01 \n", + "Iter: 8 Ap: 1.00e+00 Pobj: -5.0002547e-01 Ad: 1.00e+00 Dobj: -4.9997505e-01 \n", + "Iter: 9 Ap: 9.90e-01 Pobj: -5.0000127e-01 Ad: 1.00e+00 Dobj: -4.9999949e-01 \n", + "Iter: 10 Ap: 1.00e+00 Pobj: -5.0000006e-01 Ad: 1.00e+00 Dobj: -4.9999997e-01 \n", + "Iter: 11 Ap: 9.70e-01 Pobj: -5.0000000e-01 Ad: 9.70e-01 Dobj: -5.0000000e-01 \n", + "Success: SDP solved\n", + "Primal objective value: -5.0000000e-01 \n", + "Dual objective value: -5.0000000e-01 \n", + "Relative primal infeasibility: 9.16e-16 \n", + "Relative dual infeasibility: 1.57e-09 \n", + "Real Relative Gap: 1.08e-09 \n", + "XZ Relative Gap: 3.47e-09 \n", + "DIMACS error measures: 9.59e-16 0.00e+00 3.20e-09 0.00e+00 1.08e-09 3.47e-09\n", + "Starting the rounding of the result\n", + "Flattening X matrices\n", + "This took 0.14052486419677734s\n", + "Correcting flat X matrices\n", + "Dimensions: (2, 1)\n", + "This took 0.0032029151916503906s\n", + "Unflattening X matrices\n", + "This took 0.0007612705230712891s\n", + "Calculating resulting bound\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|████████████████████████████████████████████| 1/1 [00:00<00:00, 421.28it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "This took 0.007541656494140625s\n", + "Final rounded bound is 1/2\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/plain": [ + "1/2" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "###\n", + "### Rounding\n", + "###\n", + "\n", + "# Specifying the optimizer `exact=True` tries to round the result\n", + "G.reset()\n", + "G.exclude(G(3))\n", + "# Get exactly 1/2 for Mantel's theorem\n", + "G.optimize(G(2), 3, exact=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "83876f79-a88e-4602-9b9c-5772997a1ce7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Base flags generated, their number is 11\n", + "The relevant ftypes are constructed, their number is 2\n", + "Block sizes before symmetric/asymmetric change is applied: [4, 4]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with mult table for Ftype on 2 points with edges=(01): : 2it [00:00, 1036.65it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tables finished\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with positivity constraint 0: 100%|█████████| 1/1 [00:00<00:00, 291.86it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Constraints finished\n", + "Running sdp without construction. Used block sizes are [3, 1, 3, 1, -11, -8]\n", + "CSDP 6.2.0\n", + "Iter: 0 Ap: 0.00e+00 Pobj: 0.0000000e+00 Ad: 0.00e+00 Dobj: 0.0000000e+00 \n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Iter: 1 Ap: 1.00e+00 Pobj: -2.0351247e+01 Ad: 7.28e-01 Dobj: 1.9241068e-01 \n", + "Iter: 2 Ap: 1.00e+00 Pobj: -2.0298857e+01 Ad: 9.49e-01 Dobj: -1.1331216e-01 \n", + "Iter: 3 Ap: 1.00e+00 Pobj: -1.2166750e+01 Ad: 8.89e-01 Dobj: -1.1297865e-01 \n", + "Iter: 4 Ap: 9.44e-01 Pobj: -3.5639778e+00 Ad: 7.81e-01 Dobj: -8.8235703e-02 \n", + "Iter: 5 Ap: 1.00e+00 Pobj: -1.0447222e+00 Ad: 8.33e-01 Dobj: -8.4380440e-02 \n", + "Iter: 6 Ap: 9.75e-01 Pobj: -3.1581467e-01 Ad: 8.64e-01 Dobj: -1.0555082e-01 \n", + "Iter: 7 Ap: 1.00e+00 Pobj: -3.0596168e-01 Ad: 7.93e-01 Dobj: -2.0824259e-01 \n", + "Iter: 8 Ap: 1.00e+00 Pobj: -2.5581926e-01 Ad: 8.13e-01 Dobj: -2.3309729e-01 \n", + "Iter: 9 Ap: 1.00e+00 Pobj: -2.5041006e-01 Ad: 9.80e-01 Dobj: -2.4855614e-01 \n", + "Iter: 10 Ap: 1.00e+00 Pobj: -2.5001867e-01 Ad: 1.00e+00 Dobj: -2.4993040e-01 \n", + "Iter: 11 Ap: 1.00e+00 Pobj: -2.5000179e-01 Ad: 9.88e-01 Dobj: -2.4999529e-01 \n", + "Iter: 12 Ap: 1.00e+00 Pobj: -2.5000054e-01 Ad: 1.00e+00 Dobj: -2.4999886e-01 \n", + "Iter: 13 Ap: 1.00e+00 Pobj: -2.5000007e-01 Ad: 1.00e+00 Dobj: -2.4999989e-01 \n", + "Iter: 14 Ap: 1.00e+00 Pobj: -2.5000000e-01 Ad: 1.00e+00 Dobj: -2.4999999e-01 \n", + "Iter: 15 Ap: 9.60e-01 Pobj: -2.5000000e-01 Ad: 9.54e-01 Dobj: -2.5000000e-01 \n", + "Success: SDP solved\n", + "Primal objective value: -2.5000000e-01 \n", + "Dual objective value: -2.5000000e-01 \n", + "Relative primal infeasibility: 1.34e-14 \n", + "Relative dual infeasibility: 1.46e-10 \n", + "Real Relative Gap: 3.78e-10 \n", + "XZ Relative Gap: 7.90e-10 \n", + "DIMACS error measures: 1.46e-14 0.00e+00 3.95e-10 0.00e+00 3.78e-10 7.90e-10\n", + "The initial run gave an accurate looking construction\n", + "Rounded construction vector is: \n", + "Flag Algebra Element over Rational Field\n", + "1/8 - Flag on 4 points, ftype from () with edges=()\n", + "1/2 - Flag on 4 points, ftype from () with edges=(01 02 03)\n", + "3/8 - Flag on 4 points, ftype from () with edges=(02 03 12 13)\n", + "Adjusting table with kernels from construction\n", + "Running SDP after kernel correction. Used block sizes are [2, 1, 2, 1, -11, -8]\n", + "CSDP 6.2.0\n", + "Iter: 0 Ap: 0.00e+00 Pobj: 0.0000000e+00 Ad: 0.00e+00 Dobj: 0.0000000e+00 \n", + "Iter: 1 Ap: 1.00e+00 Pobj: -1.6899685e+01 Ad: 7.65e-01 Dobj: 7.9442214e-01 \n", + "Iter: 2 Ap: 1.00e+00 Pobj: -1.6923744e+01 Ad: 9.47e-01 Dobj: -7.7908927e-02 \n", + "Iter: 3 Ap: 1.00e+00 Pobj: -1.0216264e+01 Ad: 8.89e-01 Dobj: -8.5334539e-02 \n", + "Iter: 4 Ap: 9.71e-01 Pobj: -2.9740218e+00 Ad: 7.74e-01 Dobj: -6.3244542e-02 \n", + "Iter: 5 Ap: 1.00e+00 Pobj: -7.4813836e-01 Ad: 8.49e-01 Dobj: -6.3640609e-02 \n", + "Iter: 6 Ap: 9.71e-01 Pobj: -3.0977453e-01 Ad: 8.43e-01 Dobj: -9.2208413e-02 \n", + "Iter: 7 Ap: 1.00e+00 Pobj: -3.1279644e-01 Ad: 7.37e-01 Dobj: -1.9386842e-01 \n", + "Iter: 8 Ap: 1.00e+00 Pobj: -2.5718711e-01 Ad: 8.00e-01 Dobj: -2.2633313e-01 \n", + "Iter: 9 Ap: 1.00e+00 Pobj: -2.5046552e-01 Ad: 9.64e-01 Dobj: -2.4756687e-01 \n", + "Iter: 10 Ap: 9.98e-01 Pobj: -2.5002103e-01 Ad: 1.00e+00 Dobj: -2.4987493e-01 \n", + "Iter: 11 Ap: 1.00e+00 Pobj: -2.5000133e-01 Ad: 9.97e-01 Dobj: -2.4999411e-01 \n", + "Iter: 12 Ap: 1.00e+00 Pobj: -2.5000035e-01 Ad: 1.00e+00 Dobj: -2.4999906e-01 \n", + "Iter: 13 Ap: 1.00e+00 Pobj: -2.5000004e-01 Ad: 1.00e+00 Dobj: -2.4999988e-01 \n", + "Iter: 14 Ap: 1.00e+00 Pobj: -2.5000000e-01 Ad: 9.99e-01 Dobj: -2.4999999e-01 \n", + "Iter: 15 Ap: 9.60e-01 Pobj: -2.5000000e-01 Ad: 9.55e-01 Dobj: -2.5000000e-01 \n", + "Success: SDP solved\n", + "Primal objective value: -2.5000000e-01 \n", + "Dual objective value: -2.5000000e-01 \n", + "Relative primal infeasibility: 1.32e-14 \n", + "Relative dual infeasibility: 1.40e-10 \n", + "Real Relative Gap: 3.45e-10 \n", + "XZ Relative Gap: 7.40e-10 \n", + "DIMACS error measures: 1.43e-14 0.00e+00 3.70e-10 0.00e+00 3.45e-10 7.40e-10\n", + "Starting the rounding of the result\n", + "Flattening X matrices\n", + "This took 0.6188833713531494s\n", + "Correcting flat X matrices\n", + "Dimensions: (3, 14)\n", + "This took 0.01806020736694336s\n", + "Unflattening X matrices\n", + "This took 0.00048422813415527344s\n", + "Calculating resulting bound\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|████████████████████████████████████████████| 2/2 [00:00<00:00, 161.95it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "This took 0.01578545570373535s\n", + "Final rounded bound is 1/4\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/plain": [ + "1/4" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# If the optimal construction is well-behaved, this works well.\n", + "# From an earlier cell\n", + "G.reset()\n", + "G.optimize_problem(G(3), 4, positives=[ 1/2 - G(2, ftype=[0]) ], exact=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "e3d108b6-62c2-46d6-a1e6-326143e95562", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Base flags generated, their number is 11\n", + "The relevant ftypes are constructed, their number is 2\n", + "Block sizes before symmetric/asymmetric change is applied: [4, 4]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with mult table for Ftype on 2 points with edges=(01): : 2it [00:00, 551.41it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tables finished\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with positivity constraint 0: 100%|█████████| 1/1 [00:00<00:00, 345.27it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Constraints finished\n", + "Running sdp without construction. Used block sizes are [3, 1, 3, 1, -11, -4]\n", + "CSDP 6.2.0\n", + "Iter: 0 Ap: 0.00e+00 Pobj: 0.0000000e+00 Ad: 0.00e+00 Dobj: 0.0000000e+00 \n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Iter: 1 Ap: 1.00e+00 Pobj: -2.0362520e+01 Ad: 7.29e-01 Dobj: -6.0813905e-02 \n", + "Iter: 2 Ap: 1.00e+00 Pobj: -2.0324044e+01 Ad: 9.49e-01 Dobj: -1.4090950e-01 \n", + "Iter: 3 Ap: 1.00e+00 Pobj: -1.2792797e+01 Ad: 8.87e-01 Dobj: -1.4525769e-01 \n", + "Iter: 4 Ap: 1.00e+00 Pobj: -2.6397236e+00 Ad: 8.16e-01 Dobj: -1.4716863e-01 \n", + "Iter: 5 Ap: 9.70e-01 Pobj: -5.7645242e-01 Ad: 8.89e-01 Dobj: -1.6649882e-01 \n", + "Iter: 6 Ap: 1.00e+00 Pobj: -4.7413525e-01 Ad: 8.51e-01 Dobj: -2.9000569e-01 \n", + "Iter: 7 Ap: 1.00e+00 Pobj: -3.7538701e-01 Ad: 7.49e-01 Dobj: -3.2184161e-01 \n", + "Iter: 8 Ap: 1.00e+00 Pobj: -3.5709889e-01 Ad: 8.67e-01 Dobj: -3.4598987e-01 \n", + "Iter: 9 Ap: 1.00e+00 Pobj: -3.5385657e-01 Ad: 1.00e+00 Dobj: -3.5309874e-01 \n", + "Iter: 10 Ap: 9.99e-01 Pobj: -3.5356572e-01 Ad: 1.00e+00 Dobj: -3.5353550e-01 \n", + "Iter: 11 Ap: 9.94e-01 Pobj: -3.5355424e-01 Ad: 1.00e+00 Dobj: -3.5355362e-01 \n", + "Iter: 12 Ap: 1.00e+00 Pobj: -3.5355343e-01 Ad: 9.87e-01 Dobj: -3.5355340e-01 \n", + "Iter: 13 Ap: 9.59e-01 Pobj: -3.5355339e-01 Ad: 9.57e-01 Dobj: -3.5355339e-01 \n", + "Success: SDP solved\n", + "Primal objective value: -3.5355339e-01 \n", + "Dual objective value: -3.5355339e-01 \n", + "Relative primal infeasibility: 1.82e-15 \n", + "Relative dual infeasibility: 1.84e-09 \n", + "Real Relative Gap: 3.80e-10 \n", + "XZ Relative Gap: 3.51e-09 \n", + "DIMACS error measures: 1.98e-15 0.00e+00 4.99e-09 0.00e+00 3.80e-10 3.51e-09\n", + "The initial run didn't provide an accurate construction\n", + "Flattening X matrices\n", + "This took 0.6651790142059326s\n", + "Correcting flat X matrices\n", + "Dimensions: (3, 10)\n", + "This took 0.005103111267089844s\n", + "Unflattening X matrices\n", + "This took 0.00024056434631347656s\n", + "Calculating resulting bound\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|████████████████████████████████████████████| 2/2 [00:00<00:00, 105.20it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "This took 0.024486064910888672s\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/plain": [ + "1016709/2875648" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# If the solution does not look simple, then an approximate result is returned (usually)\n", + "G.reset()\n", + "G.optimize(triangle, 4, positives=[ 1/2 - edge ], exact=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "id": "a1d43d8f-7880-403a-b81b-dfcfe01cfee1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Base flags generated, their number is 11\n", + "The relevant ftypes are constructed, their number is 2\n", + "Block sizes before symmetric/asymmetric change is applied: [4, 4]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with mult table for Ftype on 2 points with edges=(01): : 2it [00:00, 571.31it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tables finished\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with positivity constraint 0: 100%|█████████| 1/1 [00:00<00:00, 266.93it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Constraints finished\n", + "Running sdp without construction. Used block sizes are [3, 1, 3, 1, -11, -4]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CSDP 6.2.0\n", + "Iter: 0 Ap: 0.00e+00 Pobj: 0.0000000e+00 Ad: 0.00e+00 Dobj: 0.0000000e+00 \n", + "Iter: 1 Ap: 1.00e+00 Pobj: -2.0362520e+01 Ad: 7.29e-01 Dobj: -6.0813905e-02 \n", + "Iter: 2 Ap: 1.00e+00 Pobj: -2.0324044e+01 Ad: 9.49e-01 Dobj: -1.4090950e-01 \n", + "Iter: 3 Ap: 1.00e+00 Pobj: -1.2792797e+01 Ad: 8.87e-01 Dobj: -1.4525769e-01 \n", + "Iter: 4 Ap: 1.00e+00 Pobj: -2.6397236e+00 Ad: 8.16e-01 Dobj: -1.4716863e-01 \n", + "Iter: 5 Ap: 9.70e-01 Pobj: -5.7645242e-01 Ad: 8.89e-01 Dobj: -1.6649882e-01 \n", + "Iter: 6 Ap: 1.00e+00 Pobj: -4.7413525e-01 Ad: 8.51e-01 Dobj: -2.9000569e-01 \n", + "Iter: 7 Ap: 1.00e+00 Pobj: -3.7538701e-01 Ad: 7.49e-01 Dobj: -3.2184161e-01 \n", + "Iter: 8 Ap: 1.00e+00 Pobj: -3.5709889e-01 Ad: 8.67e-01 Dobj: -3.4598987e-01 \n", + "Iter: 9 Ap: 1.00e+00 Pobj: -3.5385657e-01 Ad: 1.00e+00 Dobj: -3.5309874e-01 \n", + "Iter: 10 Ap: 9.99e-01 Pobj: -3.5356572e-01 Ad: 1.00e+00 Dobj: -3.5353550e-01 \n", + "Iter: 11 Ap: 9.94e-01 Pobj: -3.5355424e-01 Ad: 1.00e+00 Dobj: -3.5355362e-01 \n", + "Iter: 12 Ap: 1.00e+00 Pobj: -3.5355343e-01 Ad: 9.87e-01 Dobj: -3.5355340e-01 \n", + "Iter: 13 Ap: 9.59e-01 Pobj: -3.5355339e-01 Ad: 9.57e-01 Dobj: -3.5355339e-01 \n", + "Success: SDP solved\n", + "Primal objective value: -3.5355339e-01 \n", + "Dual objective value: -3.5355339e-01 \n", + "Relative primal infeasibility: 1.82e-15 \n", + "Relative dual infeasibility: 1.84e-09 \n", + "Real Relative Gap: 3.80e-10 \n", + "XZ Relative Gap: 3.51e-09 \n", + "DIMACS error measures: 1.98e-15 0.00e+00 4.99e-09 0.00e+00 3.80e-10 3.51e-09\n", + "The initial run didn't provide an accurate construction\n", + "Flattening X matrices\n", + "This took 0.612539529800415s\n", + "Correcting flat X matrices\n", + "Dimensions: (3, 10)\n", + "This took 0.00433349609375s\n", + "Unflattening X matrices\n", + "This took 0.0004324913024902344s\n", + "Calculating resulting bound\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|████████████████████████████████████████████| 2/2 [00:00<00:00, 111.73it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "This took 0.02367401123046875s\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/plain": [ + "1041104997/2944663552" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# This can be refined, to use higher denominator\n", + "G.reset()\n", + "G.optimize(triangle, 4, positives=[ 1/2 - edge ], exact=True, denom=1024*1024)" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "bb54c471-cc89-4e1b-a3ff-c2e96291b828", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Base flags generated, their number is 10\n", + "The relevant ftypes are constructed, their number is 2\n", + "Block sizes before symmetric/asymmetric change is applied: [4, 4]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with mult table for Ftype on 2 points with edges=(01): : 2it [00:00, 123.45it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tables finished\n", + "Constraints finished\n", + "Adjusting table with kernels from construction\n", + "Running SDP after kernel correction. Used block sizes are [2, 1, 2, 1, -10, -2]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CSDP 6.2.0\n", + "Iter: 0 Ap: 0.00e+00 Pobj: 0.0000000e+00 Ad: 0.00e+00 Dobj: 0.0000000e+00 \n", + "Iter: 1 Ap: 1.00e+00 Pobj: -1.5796684e+01 Ad: 7.85e-01 Dobj: 6.8056615e-01 \n", + "Iter: 2 Ap: 1.00e+00 Pobj: -1.5707513e+01 Ad: 9.45e-01 Dobj: -3.2226703e-01 \n", + "Iter: 3 Ap: 1.00e+00 Pobj: -9.0890803e+00 Ad: 8.85e-01 Dobj: -3.7904944e-01 \n", + "Iter: 4 Ap: 1.00e+00 Pobj: -2.2516380e+00 Ad: 7.96e-01 Dobj: -3.9403170e-01 \n", + "Iter: 5 Ap: 9.85e-01 Pobj: -7.9859123e-01 Ad: 8.64e-01 Dobj: -4.2692072e-01 \n", + "Iter: 6 Ap: 1.00e+00 Pobj: -7.9752960e-01 Ad: 8.05e-01 Dobj: -5.9421417e-01 \n", + "Iter: 7 Ap: 1.00e+00 Pobj: -6.7904686e-01 Ad: 8.09e-01 Dobj: -6.3478451e-01 \n", + "Iter: 8 Ap: 1.00e+00 Pobj: -6.6818649e-01 Ad: 8.96e-01 Dobj: -6.6029144e-01 \n", + "Iter: 9 Ap: 1.00e+00 Pobj: -6.6678401e-01 Ad: 1.00e+00 Dobj: -6.6632553e-01 \n", + "Iter: 10 Ap: 9.99e-01 Pobj: -6.6667180e-01 Ad: 1.00e+00 Dobj: -6.6665332e-01 \n", + "Iter: 11 Ap: 1.00e+00 Pobj: -6.6666722e-01 Ad: 9.98e-01 Dobj: -6.6666650e-01 \n", + "Iter: 12 Ap: 1.00e+00 Pobj: -6.6666670e-01 Ad: 9.85e-01 Dobj: -6.6666661e-01 \n", + "Iter: 13 Ap: 9.59e-01 Pobj: -6.6666667e-01 Ad: 9.56e-01 Dobj: -6.6666667e-01 \n", + "Success: SDP solved\n", + "Primal objective value: -6.6666667e-01 \n", + "Dual objective value: -6.6666667e-01 \n", + "Relative primal infeasibility: 2.26e-15 \n", + "Relative dual infeasibility: 1.57e-09 \n", + "Real Relative Gap: 1.31e-09 \n", + "XZ Relative Gap: 3.42e-09 \n", + "DIMACS error measures: 3.21e-15 0.00e+00 4.20e-09 0.00e+00 1.31e-09 3.42e-09\n", + "Starting the rounding of the result\n", + "Flattening X matrices\n", + "This took 0.6052815914154053s\n", + "Correcting flat X matrices\n", + "Dimensions: (4, 8)\n", + "This took 0.007422447204589844s\n", + "Unflattening X matrices\n", + "Linear coefficient is negative: -329/512\n", + "This took 0.0006260871887207031s\n", + "Calculating resulting bound\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|████████████████████████████████████████████| 2/2 [00:00<00:00, 134.67it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "This took 0.020173311233520508s\n", + "Final rounded bound is 2/3\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/plain": [ + "2/3" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "###\n", + "### Construction\n", + "###\n", + "\n", + "# When the optimizer fails to give a correct looking construction,\n", + "# It is possible to provide it directly\n", + "\n", + "# Theory.blowup_construction(target_size, construction_size, **relations)\n", + "# provides a way to specify blowups of simple patterns\n", + "\n", + "G.reset()\n", + "\n", + "# Here is a tripartite graph, with target size 4\n", + "trip_constr = G.blowup_construction(4, 3, edges=[[0, 1], [0, 2], [1, 2]])\n", + "\n", + "#Here is the complement of that graph. So only edges inside the parts\n", + "trip_constr_compl = G.blowup_construction(4, 3, edges=[[0, 0], [1, 1], [2, 2]])\n", + "\n", + "G.reset()\n", + "G.exclude(G(4))\n", + "# Note the constructions above were for Graphs with perhaps a different excluded structure.\n", + "# We need to construct it again for K4-free graphs\n", + "trip_constr_compl = G.blowup_construction(4, 3, edges=[[0, 0], [1, 1], [2, 2]])\n", + "G.optimize(G(2), 4, construction=trip_constr_compl, exact=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "id": "0ca869fb-903b-40ed-92c4-fd9de7341bb1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Symbolic construction: \n", + " Flag Algebra Element over Multivariate Polynomial Ring in x, y, z over Rational Field\n", + "x^4 + y^4 + z^4 - Flag on 4 points, ftype from () with edges=()\n", + "4*x^3*y + 4*x*y^3 + 4*x^3*z + 4*y^3*z + 4*x*z^3 + 4*y*z^3 - Flag on 4 points, ftype from () with edges=(01 02 03)\n", + "6*x^2*y^2 + 6*x^2*z^2 + 6*y^2*z^2 - Flag on 4 points, ftype from () with edges=(02 03 12 13)\n", + "12*x^2*y*z + 12*x*y^2*z + 12*x*y*z^2 - Flag on 4 points, ftype from () with edges=(01 02 03 12 13)\n", + "\n", + "\n", + "Construction at another point: \n", + " Flag Algebra Element over Rational Field\n", + "49/648 - Flag on 4 points, ftype from () with edges=()\n", + "59/162 - Flag on 4 points, ftype from () with edges=(01 02 03)\n", + "49/216 - Flag on 4 points, ftype from () with edges=(02 03 12 13)\n", + "1/3 - Flag on 4 points, ftype from () with edges=(01 02 03 12 13)\n", + "\n", + "\n", + "Construction after sum set and diff: \n", + " Flag Algebra Element over Multivariate Polynomial Ring in x, y, z over Rational Field\n", + "8*x^3 + 12*x^2*y + 12*x*y^2 + 4*y^3 - 12*x^2 - 24*x*y - 12*y^2 + 12*x + 12*y - 4 - Flag on 4 points, ftype from () with edges=()\n", + "-32*x^3 - 48*x^2*y - 48*x*y^2 - 16*y^3 + 48*x^2 + 72*x*y + 36*y^2 - 24*x - 24*y + 4 - Flag on 4 points, ftype from () with edges=(01 02 03)\n", + "24*x^3 + 36*x^2*y + 36*x*y^2 + 12*y^3 - 36*x^2 - 24*x*y - 12*y^2 + 12*x - Flag on 4 points, ftype from () with edges=(02 03 12 13)\n", + "-24*x*y - 12*y^2 + 12*y - Flag on 4 points, ftype from () with edges=(01 02 03 12 13) \n", + "\n", + "\n", + "Base flags generated, their number is 11\n", + "The relevant ftypes are constructed, their number is 2\n", + "Block sizes before symmetric/asymmetric change is applied: [4, 4]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with mult table for Ftype on 2 points with edges=(01): : 2it [00:00, 594.81it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tables finished\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with positivity constraint 0: 100%|█████████| 1/1 [00:00<00:00, 385.79it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Constraints finished\n", + "Adjusting table with kernels from construction\n", + "Running SDP after kernel correction. Used block sizes are [1, 1, 1, -11, -3]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CSDP 6.2.0\n", + "Iter: 0 Ap: 0.00e+00 Pobj: 0.0000000e+00 Ad: 0.00e+00 Dobj: 0.0000000e+00 \n", + "Iter: 1 Ap: 1.00e+00 Pobj: -1.3408760e+01 Ad: 8.19e-01 Dobj: -6.3955233e-01 \n", + "Iter: 2 Ap: 1.00e+00 Pobj: -1.3318852e+01 Ad: 9.28e-01 Dobj: -4.5726524e-01 \n", + "Iter: 3 Ap: 1.00e+00 Pobj: -8.1795916e+00 Ad: 7.94e-01 Dobj: -4.2808208e-01 \n", + "Iter: 4 Ap: 1.00e+00 Pobj: -1.9104284e+00 Ad: 8.04e-01 Dobj: -4.1569778e-01 \n", + "Iter: 5 Ap: 9.55e-01 Pobj: -6.9836150e-01 Ad: 8.94e-01 Dobj: -4.4765946e-01 \n", + "Iter: 6 Ap: 1.00e+00 Pobj: -5.9772430e-01 Ad: 9.53e-01 Dobj: -5.4377973e-01 \n", + "Iter: 7 Ap: 1.00e+00 Pobj: -5.7362396e-01 Ad: 9.01e-01 Dobj: -5.6678083e-01 \n", + "Iter: 8 Ap: 1.00e+00 Pobj: -5.7155774e-01 Ad: 9.90e-01 Dobj: -5.7120497e-01 \n", + "Iter: 9 Ap: 1.00e+00 Pobj: -5.7143532e-01 Ad: 1.00e+00 Dobj: -5.7141901e-01 \n", + "Iter: 10 Ap: 9.84e-01 Pobj: -5.7142917e-01 Ad: 1.00e+00 Dobj: -5.7142841e-01 \n", + "Iter: 11 Ap: 1.00e+00 Pobj: -5.7142860e-01 Ad: 9.74e-01 Dobj: -5.7142855e-01 \n", + "Iter: 12 Ap: 9.59e-01 Pobj: -5.7142857e-01 Ad: 9.59e-01 Dobj: -5.7142857e-01 \n", + "Success: SDP solved\n", + "Primal objective value: -5.7142857e-01 \n", + "Dual objective value: -5.7142857e-01 \n", + "Relative primal infeasibility: 2.64e-15 \n", + "Relative dual infeasibility: 1.59e-09 \n", + "Real Relative Gap: 7.87e-10 \n", + "XZ Relative Gap: 2.83e-09 \n", + "DIMACS error measures: 3.82e-15 0.00e+00 3.74e-09 0.00e+00 7.87e-10 2.83e-09\n" + ] + }, + { + "data": { + "text/plain": [ + "0.571428572586048" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# It is possible to create symbolic constructions\n", + "G.reset()\n", + "R. = QQ[]\n", + "trip_constr = G.blowup_construction(4, [x, y, z], edges=[[0, 1], [0, 2], [1, 2]])\n", + "\n", + "# Here the construction is a flag over the rationals extended with 3 variables X0, X1, X2 (one for each part)\n", + "print(\"Symbolic construction: \\n\", trip_constr)\n", + "\n", + "# To evaluate it at a given point, we can call\n", + "eval_constr = trip_constr.subs([1/2, 1/3, 1/6])\n", + "print(\"\\n\\nConstruction at another point: \\n\", eval_constr)\n", + "\n", + "# It is also possible to force the sum to be 1\n", + "sumset_constr = trip_constr.set_sum()\n", + "\n", + "# We can also differentiate each variable a given number of times\n", + "der_sumset_constr = sumset_constr.derivative([1, 0, 0])\n", + "print(\"\\n\\nConstruction after sum set and diff: \\n\", der_sumset_constr, \"\\n\\n\")\n", + "\n", + "# Probably the most useful function is to differentiate in all possible way and\n", + "# Substitute at a given point\n", + "# This returns a list of constructions (with some scaling)\n", + "constrs = trip_constr.derivatives([1/2, 1/3, 1/6])\n", + "# It is possible to pass multiple constructions to the optimizer\n", + "G.optimize(G(2), 4, construction=constrs, positives=[-G(3)])" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "id": "74d34638-ba0e-44ce-8386-d307aebcb8cf", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Base flags generated, their number is 3\n", + "The relevant ftypes are constructed, their number is 1\n", + "Block sizes before symmetric/asymmetric change is applied: [2]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with mult table for Ftype on 1 points with edges=(): : 1it [00:00, 456.10it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tables finished\n", + "Constraints finished\n", + "Running sdp without construction. Used block sizes are [2, -3, -2]\n", + "CSDP 6.2.0\n", + "Iter: 0 Ap: 0.00e+00 Pobj: 0.0000000e+00 Ad: 0.00e+00 Dobj: 0.0000000e+00 \n", + "Iter: 1 Ap: 1.00e+00 Pobj: -1.5751371e+01 Ad: 7.93e-01 Dobj: -1.8596991e-01 \n", + "Iter: 2 Ap: 1.00e+00 Pobj: -1.3829292e+01 Ad: 9.44e-01 Dobj: -2.5639377e-01 \n", + "Iter: 3 Ap: 1.00e+00 Pobj: -3.6548137e+00 Ad: 9.22e-01 Dobj: -2.8097573e-01 \n", + "Iter: 4 Ap: 1.00e+00 Pobj: -6.7101882e-01 Ad: 8.42e-01 Dobj: -3.1080817e-01 \n", + "Iter: 5 Ap: 1.00e+00 Pobj: -5.8576221e-01 Ad: 8.44e-01 Dobj: -4.8002517e-01 \n", + "Iter: 6 Ap: 1.00e+00 Pobj: -5.0647693e-01 Ad: 8.85e-01 Dobj: -4.9296282e-01 \n", + "Iter: 7 Ap: 1.00e+00 Pobj: -5.0060021e-01 Ad: 9.41e-01 Dobj: -4.9923796e-01 \n", + "Iter: 8 Ap: 1.00e+00 Pobj: -5.0005080e-01 Ad: 1.00e+00 Dobj: -4.9996691e-01 \n", + "Iter: 9 Ap: 1.00e+00 Pobj: -5.0000234e-01 Ad: 1.00e+00 Dobj: -4.9999913e-01 \n", + "Iter: 10 Ap: 1.00e+00 Pobj: -5.0000016e-01 Ad: 1.00e+00 Dobj: -5.0000000e-01 \n", + "Iter: 11 Ap: 9.58e-01 Pobj: -5.0000001e-01 Ad: 9.70e-01 Dobj: -5.0000000e-01 \n", + "Success: SDP solved\n", + "Primal objective value: -5.0000001e-01 \n", + "Dual objective value: -5.0000000e-01 \n", + "Relative primal infeasibility: 5.33e-16 \n", + "Relative dual infeasibility: 3.98e-09 \n", + "Real Relative Gap: 2.49e-09 \n", + "XZ Relative Gap: 9.08e-09 \n", + "DIMACS error measures: 5.58e-16 0.00e+00 8.28e-09 0.00e+00 2.49e-09 9.08e-09\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/plain": [ + "0.5000000069458745" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "###\n", + "### Certificates\n", + "###\n", + "\n", + "# The optimizer can save the certificates to a provided file\n", + "G.reset()\n", + "G.exclude(G(3))\n", + "# The format could be either an exact rational solution, or an approximate like this\n", + "G.optimize(G(2), 3, file=\"test\")" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "a348d7a4-ef0b-49e4-9a30-296380046620", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Checking X matrices\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "1it [00:00, 1826.79it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Solution matrices are all positive semidefinite, linear coefficients are all non-negative\n", + "Calculating multiplication tables\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "1it [00:00, 1899.59it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Done calculating linear constraints\n", + "Calculating the bound provided by the certificate\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "1it [00:00, 1646.12it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The solution is valid, it proves the bound 0.500000003858861\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/plain": [ + "0.500000003858861" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# A given certificate can be verified\n", + "\n", + "# Note that the problem we verify the certificate for has to be the same\n", + "# This includes target value, target size, positives, excluded structures.\n", + "# The construction used for rounding is not needed here\n", + "\n", + "G.reset()\n", + "G.exclude(G(3))\n", + "G.verify(\"test\", G(2), 3)" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "id": "dd48bad4-194a-45a3-b484-e814236b29ae", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Base flags generated, their number is 3\n", + "The relevant ftypes are constructed, their number is 1\n", + "Block sizes before symmetric/asymmetric change is applied: [2]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with mult table for Ftype on 1 points with edges=(): : 1it [00:00, 919.80it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tables finished\n", + "Constraints finished\n", + "Relevant ftypes up to size 4\n", + " [(3, Ftype on 2 points with edges=(), 4), (3, Ftype on 2 points with edges=(01), 4)]\n", + "Base flags generated, their number is 7\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The relevant ftypes are constructed, their number is 1\n", + "Block sizes before symmetric/asymmetric change is applied: [3]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with mult table for Ftype on 2 points with edges=(): : 1it [00:00, 8.29it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tables finished\n", + "Constraints finished\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "###\n", + "### Exporting the SDP problem\n", + "###\n", + "\n", + "# It is possible to create an SDP problem instance and solve that on a different machine\n", + "G.reset()\n", + "G.exclude(G(3))\n", + "# The parameters are the same as for the regular optimize, \n", + "# except it can't do an exact solution and requires a file name to write the problem to.\n", + "G.external_optimize(G(2), 3, file=\"problem\")\n", + "\n", + "# If the problem is too large, it is possible to export with only a few types.\n", + "# The following command lists the relevant ftypes appearing in an optimization with target size 4\n", + "print(\"Relevant ftypes up to size 4\\n\", G._get_relevant_ftypes(4))\n", + "\n", + "# Choosing a subset of this can be passed to the external optimize like this\n", + "\n", + "types_relevant = [G._get_relevant_ftypes(4)[0]]\n", + "G.external_optimize(G(2), 4, file=\"problem_2\", specific_ftype=types_relevant)" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "814fef6b-9040-4446-907f-da2d2242e7f4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Base flags generated, their number is 11\n", + "The relevant ftypes are constructed, their number is 2\n", + "Block sizes before symmetric/asymmetric change is applied: [4, 4]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with mult table for Ftype on 2 points with edges=(01): : 2it [00:00, 504.30it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tables finished\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with positivity constraint 0: 100%|█████████| 1/1 [00:00<00:00, 176.11it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Constraints finished\n", + "Running sdp without construction. Used block sizes are [3, 1, 3, 1, -11, -4]\n", + "CSDP 6.2.0\n", + "Iter: 0 Ap: 0.00e+00 Pobj: 0.0000000e+00 Ad: 0.00e+00 Dobj: 0.0000000e+00 \n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Iter: 1 Ap: 1.00e+00 Pobj: -2.0362520e+01 Ad: 7.29e-01 Dobj: -6.0813905e-02 \n", + "Iter: 2 Ap: 1.00e+00 Pobj: -2.0324044e+01 Ad: 9.49e-01 Dobj: -1.4090943e-01 \n", + "Iter: 3 Ap: 1.00e+00 Pobj: -1.2792868e+01 Ad: 8.87e-01 Dobj: -1.4525769e-01 \n", + "Iter: 4 Ap: 1.00e+00 Pobj: -2.6397598e+00 Ad: 8.16e-01 Dobj: -1.4716860e-01 \n", + "Iter: 5 Ap: 9.72e-01 Pobj: -5.7583916e-01 Ad: 8.88e-01 Dobj: -1.6648294e-01 \n", + "Iter: 6 Ap: 1.00e+00 Pobj: -4.7426918e-01 Ad: 8.51e-01 Dobj: -2.9014271e-01 \n", + "Iter: 7 Ap: 1.00e+00 Pobj: -3.7535668e-01 Ad: 7.50e-01 Dobj: -3.2194950e-01 \n", + "Iter: 8 Ap: 1.00e+00 Pobj: -3.5708921e-01 Ad: 8.67e-01 Dobj: -3.4601216e-01 \n", + "Iter: 9 Ap: 1.00e+00 Pobj: -3.5385242e-01 Ad: 1.00e+00 Dobj: -3.5310489e-01 \n", + "Iter: 10 Ap: 9.99e-01 Pobj: -3.5356555e-01 Ad: 1.00e+00 Dobj: -3.5353581e-01 \n", + "Iter: 11 Ap: 9.94e-01 Pobj: -3.5355424e-01 Ad: 1.00e+00 Dobj: -3.5355363e-01 \n", + "Iter: 12 Ap: 1.00e+00 Pobj: -3.5355343e-01 Ad: 9.87e-01 Dobj: -3.5355340e-01 \n", + "Iter: 13 Ap: 9.59e-01 Pobj: -3.5355339e-01 Ad: 9.57e-01 Dobj: -3.5355339e-01 \n", + "Success: SDP solved\n", + "Primal objective value: -3.5355339e-01 \n", + "Dual objective value: -3.5355339e-01 \n", + "Relative primal infeasibility: 2.04e-15 \n", + "Relative dual infeasibility: 1.83e-09 \n", + "Real Relative Gap: 3.80e-10 \n", + "XZ Relative Gap: 3.49e-09 \n", + "DIMACS error measures: 2.22e-15 0.00e+00 4.96e-09 0.00e+00 3.80e-10 3.49e-09\n", + "The initial run didn't provide an accurate construction\n", + "Flattening X matrices\n", + "This took 0.6994287967681885s\n", + "Correcting flat X matrices\n", + "Dimensions: (3, 10)\n", + "This took 0.0038390159606933594s\n", + "Unflattening X matrices\n", + "This took 0.0002505779266357422s\n", + "Calculating resulting bound\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|████████████████████████████████████████████| 2/2 [00:00<00:00, 119.07it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "This took 0.021914958953857422s\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/plain": [ + "1016709/2875648" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "###\n", + "### Rounding over larger fields\n", + "###\n", + "\n", + "# It is possible to specify a field extension of the rational\n", + "# And perform the rounding there\n", + "\n", + "# Recall the problem\n", + "G.reset()\n", + "G.optimize(G(3), 4, positives=[ 1/2 - G(2) ], exact=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "id": "ed323bdd-46c1-461a-a7f3-fae76c1c050c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Base flags generated, their number is 11\n", + "The relevant ftypes are constructed, their number is 2\n", + "Block sizes before symmetric/asymmetric change is applied: [4, 4]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with mult table for Ftype on 2 points with edges=(01): : 2it [00:00, 818.00it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tables finished\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Done with positivity constraint 0: 100%|█████████| 1/1 [00:00<00:00, 373.32it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Constraints finished\n", + "Adjusting table with kernels from construction\n", + "Running SDP after kernel correction. Used block sizes are [2, 1, 1, -11, -4]\n", + "CSDP 6.2.0\n", + "Iter: 0 Ap: 0.00e+00 Pobj: 0.0000000e+00 Ad: 0.00e+00 Dobj: 0.0000000e+00 \n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Iter: 1 Ap: 1.00e+00 Pobj: -1.5710306e+01 Ad: 7.86e-01 Dobj: 1.5423449e+00 \n", + "Iter: 2 Ap: 1.00e+00 Pobj: -1.5648648e+01 Ad: 9.46e-01 Dobj: -2.6486804e-02 \n", + "Iter: 3 Ap: 1.00e+00 Pobj: -9.4555323e+00 Ad: 8.85e-01 Dobj: -8.6856597e-02 \n", + "Iter: 4 Ap: 1.00e+00 Pobj: -1.5831052e+00 Ad: 8.29e-01 Dobj: -9.5169021e-02 \n", + "Iter: 5 Ap: 9.58e-01 Pobj: -5.1183501e-01 Ad: 8.57e-01 Dobj: -1.2754711e-01 \n", + "Iter: 6 Ap: 1.00e+00 Pobj: -4.6540901e-01 Ad: 7.67e-01 Dobj: -2.6233982e-01 \n", + "Iter: 7 Ap: 1.00e+00 Pobj: -3.8486853e-01 Ad: 7.35e-01 Dobj: -3.0767273e-01 \n", + "Iter: 8 Ap: 1.00e+00 Pobj: -3.5735741e-01 Ad: 8.69e-01 Dobj: -3.4221005e-01 \n", + "Iter: 9 Ap: 1.00e+00 Pobj: -3.5381039e-01 Ad: 1.00e+00 Dobj: -3.5279892e-01 \n", + "Iter: 10 Ap: 9.95e-01 Pobj: -3.5356425e-01 Ad: 1.00e+00 Dobj: -3.5352377e-01 \n", + "Iter: 11 Ap: 9.92e-01 Pobj: -3.5355404e-01 Ad: 1.00e+00 Dobj: -3.5355307e-01 \n", + "Iter: 12 Ap: 1.00e+00 Pobj: -3.5355342e-01 Ad: 9.97e-01 Dobj: -3.5355338e-01 \n", + "Iter: 13 Ap: 9.60e-01 Pobj: -3.5355339e-01 Ad: 9.57e-01 Dobj: -3.5355339e-01 \n", + "Success: SDP solved\n", + "Primal objective value: -3.5355339e-01 \n", + "Dual objective value: -3.5355339e-01 \n", + "Relative primal infeasibility: 1.63e-15 \n", + "Relative dual infeasibility: 2.14e-09 \n", + "Real Relative Gap: 3.25e-10 \n", + "XZ Relative Gap: 4.23e-09 \n", + "DIMACS error measures: 1.77e-15 0.00e+00 5.18e-09 0.00e+00 3.25e-10 4.23e-09\n", + "Starting the rounding of the result\n", + "Flattening X matrices\n", + "This took 0.5871376991271973s\n", + "Correcting flat X matrices\n", + "Dimensions: (4, 7)\n", + "This took 0.011298179626464844s\n", + "Unflattening X matrices\n", + "This took 0.0006353855133056641s\n", + "Calculating resulting bound\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|█████████████████████████████████████████████| 2/2 [00:00<00:00, 56.91it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "This took 0.044979095458984375s\n", + "Final rounded bound is 1/4*sqrt2\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/plain": [ + "1/4*sqrt2" + ] + }, + "execution_count": 61, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# This, even when ran with exact=True fails to give a correct value, \n", + "# as the precise number is not rational\n", + "# to perform the same calculation in a larger field, we can specify it\n", + "\n", + "# Rationals extended with sqrt(2)\n", + "TargetRing = QQ[sqrt(2)]\n", + "\n", + "# Optimal construction\n", + "R = QQ[sqrt(2)]\n", + "s2 = R(sqrt(2))\n", + "constr = G.blowup_construction(4, [1/s2, 1-1/s2], edges=[[0, 1], [1, 1]])\n", + "# Optimizer also in the extended field\n", + "G.optimize(G(3), 4, positives=[1/2 - G(2)], construction=constr, exact=True)" + ] + }, + { + "cell_type": "markdown", + "id": "49e99676-293b-4d42-bfa7-3a70cb51d4dc", + "metadata": {}, + "source": [ + "Miscallenous\n", + "============" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "id": "8eec3474-134b-42e2-9dcb-eb03a2746059", + "metadata": {}, + "outputs": [], + "source": [ + "###\n", + "### Useful functions \n", + "###\n", + "\n", + "\n", + "# for theories\n", + "G.empty() # provides the empty element of the theory\n", + "G.generate(3, G(2, ftype=[0, 1])) # generates flags with given size and type\n", + "G.exclude([G(3), G(2)]) # sets the excluded structures\n", + "G.reset() # resets the excluded structures\n", + "#G.clear() # clears all cached calculations" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "id": "b346fa75-41b4-4c29-9885-164a35c3a057", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "True\n", + "True\n" + ] + } + ], + "source": [ + "# For flags\n", + "\n", + "e2 = G(2, edges=[], ftype=[0])\n", + "\n", + "e2 + e2\n", + "e2 * e2\n", + "test = e2 << 2\n", + "test.size()\n", + "e2.blocks()\n", + "\n", + "pe2 = G(2, ftype=[])\n", + "print(pe2.afae() == pe2.project())\n", + "print(pe2.mul_project(pe2) == (pe2*pe2).project())" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "id": "a01e66c4-3918-450b-b498-cae7e7f11538", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Flag Algebra with Ftype on 0 points with edges=() over Rational Field \n", + " Flag Algebra with Ftype on 1 points with edges=() over Rational Field \n", + " Flag Algebra with Ftype on 0 points with edges=() over Real Field with 53 bits of precision \n", + " Flag Algebra with Ftype on 0 points with edges=() over Univariate Polynomial Ring in x over Rational Field\n" + ] + } + ], + "source": [ + "# The FlagAlgebra objects\n", + "\n", + "G = GraphTheory\n", + "alg = FlagAlgebra(G, QQ)\n", + "\n", + "point = G(1, ftype=[0])\n", + "alg_pointed = FlagAlgebra(G, QQ, point)\n", + "\n", + "alg_real = FlagAlgebra(G, RR)\n", + "\n", + "alg_poly = FlagAlgebra(G, QQ[\"x\"])\n", + "\n", + "print(alg, \"\\n\", alg_pointed, \"\\n\", alg_real, \"\\n\", alg_poly)" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "id": "4f5b451e-f39b-47c8-8d94-6ef23a75a1c0", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Flag Algebra Element over Univariate Polynomial Ring in x over Rational Field\n", + "x^2 + 3/2*x + 1/2 - Flag on 4 points, ftype from () with edges=()\n", + "x^2 + 7/6*x + 1/4 - Flag on 4 points, ftype from () with edges=(01)\n", + "x^2 + 5/6*x - Flag on 4 points, ftype from () with edges=(01 03)\n", + "x^2 + 5/6*x + 1/3 - Flag on 4 points, ftype from () with edges=(02 13)\n", + "x^2 + 1/2*x - 1/4 - Flag on 4 points, ftype from () with edges=(01 02 03)\n", + "x^2 + 1/2*x - 1/4 - Flag on 4 points, ftype from () with edges=(01 03 13)\n", + "x^2 + 1/2*x + 1/12 - Flag on 4 points, ftype from () with edges=(01 02 13)\n", + "x^2 + 1/6*x - 1/6 - Flag on 4 points, ftype from () with edges=(01 02 03 13)\n", + "x^2 + 1/6*x + 1/6 - Flag on 4 points, ftype from () with edges=(02 03 12 13)\n", + "x^2 - 1/6*x - 1/12 - Flag on 4 points, ftype from () with edges=(01 02 03 12 13)\n", + "x^2 - 1/2*x - Flag on 4 points, ftype from () with edges=(01 02 03 12 13 23)" + ] + }, + "execution_count": 65, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Calculations over polynomial rings\n", + "\n", + "x = alg_poly(x)\n", + "(x + G(2)) * (G(2, edge=[[0, 1]]) - 1/2 + x)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "SageMath 10.5.beta7", + "language": "sage", + "name": "sagemath" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/doc/en/reference/algebras/index.rst b/src/doc/en/reference/algebras/index.rst index 621767627b5..a35ea119c90 100644 --- a/src/doc/en/reference/algebras/index.rst +++ b/src/doc/en/reference/algebras/index.rst @@ -71,6 +71,8 @@ Named associative algebras sage/algebras/steenrod/steenrod_algebra_mult sage/algebras/weyl_algebra sage/algebras/yangian + sage/algebras/flag_algebras + sage/algebras/flag Hecke algebras -------------- diff --git a/src/doc/en/reference/references/index.rst b/src/doc/en/reference/references/index.rst index 6bc9946a442..80795883d25 100644 --- a/src/doc/en/reference/references/index.rst +++ b/src/doc/en/reference/references/index.rst @@ -1056,6 +1056,9 @@ REFERENCES: Canadian Mathematical Society Proceedings, 2, Part 1. Providence 1982. ISBN 978-0-8218-6003-8. +.. [Bod2023] Levente Bodnár, *Generalized Tur\'an problem for Complete + Hypergraphs*, Preprint, :arxiv:`2302.07571`, (2023) + .. [Bond2007] P. Bonderson, Nonabelian anyons and interferometry, Dissertation (2007). https://thesis.library.caltech.edu/2447/ @@ -1611,6 +1614,10 @@ REFERENCES: .. [Conr] Keith Conrad, "Artin-Hasse-Type Series and Roots of Unity", http://www.math.uconn.edu/~kconrad/blurbs/gradnumthy/AHrootofunity.pdf +.. [CoRa2015] Leonardo Nagami Coregliano, Alexander A. Razborov, *On the + Density of Transitive Tournaments*, Preprint, + :arxiv:`1501.04074` (2015) + .. [Coron2023] Basile Coron *Supersolvability of built lattices and Koszulness of generalized Chow rings*. Preprint, :arxiv:`2302.13072` (2023). @@ -4379,6 +4386,11 @@ REFERENCES: .. [Lin1999] \J. van Lint, Introduction to coding theory, 3rd ed., Springer-Verlag GTM, 86, 1999. +.. [LiPf2021] Bernard Lidický, Florian Pfender, *Semidefinite + Programming and Ramsey Numbers*, SIAM Journal on + Discrete Mathematics, volume 35, 2021, pp. 2328--2344, + http://dx.doi.org/10.1137/18M1169473 + .. [Liv1993] Charles Livingston, *Knot Theory*, Carus Mathematical Monographs, number 24. @@ -5562,6 +5574,10 @@ REFERENCES: **75** (1997). 99-133. :arxiv:`math/9511223v1`. http://www.ms.unimelb.edu.au/~ram/Publications/1997PLMSv75p99.pdf +.. [Raz2007] Alexander \A. Razborov, *Flag algebras*, Journal of Symbolic Logic + Volume 72, 2007, pp. 1239--1282, + https://people.cs.uchicago.edu/~razborov/files/flag.pdf + .. [RCES1994] Ruskey, R. Cohen, P. Eades, A. Scott. *Alley CATs in search of good homes.* Congressus numerantium, 1994. diff --git a/src/sage/algebras/all.py b/src/sage/algebras/all.py index 0e995a677ec..c1c2191e7c2 100644 --- a/src/sage/algebras/all.py +++ b/src/sage/algebras/all.py @@ -29,6 +29,9 @@ from sage.algebras.quantum_groups.all import * from sage.algebras.lie_conformal_algebras.all import * +from sage.algebras.flag_algebras import * +from sage.algebras.combinatorial_theory import * + # Algebra base classes from sage.algebras.free_algebra import FreeAlgebra from sage.algebras.free_algebra_quotient import FreeAlgebraQuotient diff --git a/src/sage/algebras/combinatorial_theory.py b/src/sage/algebras/combinatorial_theory.py new file mode 100644 index 00000000000..e3ed52fddee --- /dev/null +++ b/src/sage/algebras/combinatorial_theory.py @@ -0,0 +1,4480 @@ +r""" +Combinatorial theories for flag algebras +======================================= + +This module implements **combinatorial theories** (a.k.a. *hereditary classes*) +and the main flag-algebraic workflows on them: generating flags, excluding +forbidden induced subflags, building optimization problems, and exporting / +verifying certificates, rounding and extracting exact proofs from them. + +A *combinatorial theory* is specified by one (or more) finitary relations +(e.g. edges of a graph, hyperedges of a 3-graph, colors as unary relations), +together with the convention that all flags are **induced**: any relation not +listed in the input is treated as *absent*. This makes exclusion hereditary: +if a structure is excluded, every larger flag containing it as an induced +subflag is excluded as well. + +Quick start (Mantel) +-------------------- + +The following is the typical workflow: build a theory, create flags, exclude +forbidden configurations, and optimize a density. + +:: + + sage: G = GraphTheory + sage: triangle = G(3, edges=[[0,1],[0,2],[1,2]]) + sage: edge = G(2, edges=[[0,1]]) + sage: G.exclude(triangle) + sage: G.optimize(edge, 3, exact=True) # not tested + +Creating theories +----------------- + +Use :func:`Theory` to create a new combinatorial theory. The arguments describe +the relation: + +- ``relation_name``: keyword used when constructing flags (default: ``"edges"``) +- ``arity``: size of tuples in the relation (default: 2) +- ``is_ordered``: whether tuples are ordered (directed) (default: ``False``) + +:: + + sage: G = Theory("Graph") # undirected graphs + sage: H = Theory("ThreeGraph", arity=3) # 3-uniform hypergraphs + sage: D = Theory("DiGraph", arity=2, is_ordered=True, relation_name="arc") + +Several commonly used theories are also provided as globals, e.g. +``GraphTheory``, ``ThreeGraphTheory``, ``DiGraphTheory``, etc. + +Excluding and generating +------------------------ + +Exclusion is incremental and in-place on the theory object. + +:: + + sage: G = GraphTheory + sage: G.reset() + sage: G.exclude(G(3)) # exclude the empty triple + sage: flags = G.generate(4) + sage: len(flags) > 0 + True + +Generating with a fixed type is also supported: + +:: + + sage: G.reset() + sage: edgetype = G(2, edges=[[0,1]], ftype=[0,1]) + sage: typed_flags = G.generate(4, edgetype) + sage: all(f.ftype() == edgetype for f in typed_flags) + True + +Combining theories +------------------ + +Theories can be combined into a single product theory. To avoid keyword +collisions, the component theories must use distinct ``relation_name`` values. + +:: + + sage: G = GraphTheory + sage: TG = Theory("OtherThreeGraph", arity=3, relation_name="edges3") + sage: CT = combine("TwoThreeGraph", G, TG) + sage: f = CT(3, edges=[[0,1],[0,2]], edges3=[[0,1,2]]) + sage: f.size() + 3 + +Optimization interface +---------------------- + +The main function is :meth:`CombinatorialTheory.optimize`. It supports: + +- maximization/minimization (``maximize=``) +- positivity assumptions (``positives=[...]``) +- rounding (``exact=True``) +- user-supplied constructions (``construction=...``) +- saving/verifying certificates (``file=...`` + :meth:`verify`) +- exporting the SDP instance (:meth:`external_optimize`) + +Examples: + +:: + + sage: G = GraphTheory + sage: G.reset() + sage: triangle = G(3, edges=[[0,1],[0,2],[1,2]]) + sage: edge = G(2, edges=[[0,1]]) + sage: G.optimize(triangle, 4, positives=[1/2 - edge]) # not tested + +Exporting a problem for another machine: + +:: + + sage: G.reset() + sage: G.exclude(G(3)) + sage: G.external_optimize(G(2), 3, file="problem") # not tested + +Certificates: + +:: + + sage: G.reset() + sage: G.exclude(G(3)) + sage: G.optimize(G(2), 3, file="mantel_cert") # not tested + sage: G.verify("mantel_cert") # not tested + + +References +---------- + +.. [Raz2007] A. Razborov, *Flag algebras*, J. Symbolic Logic 72 (2007), 1239-1282. + +.. SEEALSO:: + + :func:`Theory` + :class:`CombinatorialTheory` + :func:`combine` + :meth:`CombinatorialTheory.exclude` + :meth:`CombinatorialTheory.generate` + :meth:`CombinatorialTheory.optimize` + +AUTHORS: + +- Levente Bodnar (2023-2025): Main development + +""" + +# **************************************************************************** +# Copyright (C) 2023 LEVENTE BODNAR +# +# 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 2 of the License, or +# (at your option) any later version. +# https://www.gnu.org/licenses/ +# **************************************************************************** + +import itertools + +from sage.structure.unique_representation import UniqueRepresentation +from sage.structure.parent import Parent +from sage.all import QQ, NN, Integer, ZZ, infinity, RR, RDF, RealField +from sage.algebras.flag import BuiltFlag, ExoticFlag, Pattern, inductive_generator, overlap_generator +from sage.algebras.flag_algebras import FlagAlgebra, FlagAlgebraElement + +from sage.categories.sets_cat import Sets +from sage.all import vector, matrix, diagonal_matrix + +from sage.misc.prandom import randint +from sage.arith.misc import falling_factorial, binomial, factorial +from sage.misc.functional import round +from sage.functions.other import ceil +from functools import lru_cache + +import hashlib +import pickle +import os +import re, ast +import subprocess +from tqdm import tqdm + + +# Primitive rounding methods +def _flatten_matrix(mat, doubled=False): + r""" + Flatten a symmetric matrix, optionally double non-diagonal elements + """ + res = [] + factor = 2 if doubled else 1 + try: + for ii in range(len(mat)): + res.append(mat[ii][ii]) + res += [factor*mat[ii][jj] for jj in range(ii+1, len(mat))] + except: + for ii in range(len(mat)): + res.append(mat[ii]) + res += [0 for jj in range(ii+1, len(mat))] + return res + +def _unflatten_matrix(ls, dim=-1, doubled=False, upper=False): + r""" + Unflatten a symmetric matrix, optionally correct for the doubled + non-diagonal elements + """ + if dim==-1: + dim = Integer(round((1/2) * ((8*len(ls)+1)**(1/2) - 1) )) + mat = [[0]*dim for ii in range(dim)] + factor = 2 if doubled else 1 + index = 0 + for ii in range(dim): + # Fill the diagonal element + mat[ii][ii] = ls[index] + index += 1 + # Fill the off-diagonal elements + for jj in range(ii + 1, dim): + mat[ii][jj] = ls[index] / factor + if not upper: + mat[jj][ii] = ls[index] / factor + index += 1 + return matrix(mat), ls[index:] + +def _round(value, method=1, quotient_bound=7, denom_bound=9, + denom=1024): + r""" + Helper function, to round a number using either + method=0 - simple fixed denominator + method=1 - continued fractions + """ + if method==0: + return QQ(round(value*denom)/denom) + else: + from sage.rings.continued_fraction import continued_fraction + cf = continued_fraction(value) + for ii, xx in enumerate(cf.quotients()): + if xx>=2**quotient_bound or cf.denominator(ii)>2**(denom_bound): + if ii>0: + return cf.convergent(ii-1) + return 0 + return cf.value() + +def _round_list(ls, force_pos=False, method=1, quotient_bound=7, + denom_bound=9, denom=1024): + r""" + Helper function, to round a list + """ + if force_pos: + return [max( + _round(xx, method, quotient_bound, denom_bound, denom), 0 + ) for xx in ls] + else: + return [_round(xx, method, quotient_bound, + denom_bound, denom) for xx in ls] + +def _round_matrix(mat, method=1, quotient_bound=7, denom_bound=9, + denom=1024): + r""" + Helper function, to round a matrix + """ + try: + return matrix(QQ, [_round_list(xx, False, + method, quotient_bound, + denom_bound, denom + ) for xx in mat]) + except: + #This happens when a semidef constraint turns out to be just linear + return diagonal_matrix(QQ, _round_list(mat, True, + method, quotient_bound, + denom_bound, denom)) + +def _round_adaptive(ls, onevec, denom=1024): + r""" + Adaptive rounding based on continued fraction and preserving + an inner product with `onevec` + + If the continued fraction rounding fails fall back to a simple + denominator method + """ + best_vec = None + best_error = 1000 + best_lcm = 1000000000 + + orig = vector(ls) + for resol1 in range(5, 20): + resol2 = round(resol1*1.5) + rls = vector([_round(xx, quotient_bound=resol1, denom_bound=resol2) \ + for xx in orig]) + ip = rls*onevec + if ip != 0 and abs(ip - 1) best_lcm**1.5 and ip != 1: + continue + best_vec = rls/ip + best_error = abs(ip - 1) + best_lcm = ip.as_integer_ratio()[1] + if best_vec==None: + rvec = vector(QQ, _round_list(ls, True, method=0, denom=denom)) + best_vec = rvec/(rvec*onevec) + return best_vec, ((best_vec-orig)/(len(orig)**0.5)).norm() + +def _remove_kernel(mat, factor=1024, threshold=1e-4): + r""" + Kernel removal method for matrices. + + Uses the LLL algorithm to identify kernels of a given matrix with simple + rational base vectors and returns a matrix where this space is quotiented + out and a matrix that can map back to the original space. + """ + d = mat.nrows() + M_scaled = matrix(ZZ, [[round(xx*factor) for xx in vv] for vv in mat]) + M_augmented = matrix.identity(ZZ, d).augment(M_scaled) + LLL_reduced = M_augmented.LLL() + LLL_coeffs = LLL_reduced[:, :d] + norm_test = LLL_coeffs * mat + #print("norms are: ", " ".join([str(int(log(rr.norm(1)/d, 10))) for rr in norm_test])) + kernel_base = [LLL_coeffs[ii] for ii,rr in enumerate(norm_test) if rr.norm(1)/d < threshold] + #print("resulting kernel base is:\n", kernel_base) + if len(kernel_base)==0: + return mat, matrix.identity(d, sparse=True) + K = matrix(ZZ, kernel_base).stack(matrix.identity(d)) + image_space = matrix(K.gram_schmidt()[0][len(kernel_base):, :], sparse=True) + kernel_removed_mat = image_space * mat * image_space.T + norm_factor = image_space * image_space.T + mat_recover = image_space.T * norm_factor.inverse() + return kernel_removed_mat, mat_recover + +def _custom_psd_test(M): + r""" + Tests if a matrix is positive semi-definite. + + Uses the same method as an ldlt decomposition. The sage default + test works similarly but requres stronger conditions on the field. + """ + if not M.is_symmetric(): + return False + R = M.base_ring() + n = M.nrows() + L = matrix.identity(R, n) + d_diag = vector(R, n) + + for j in range(n): + v = vector([L[j, k] * d_diag[k] for k in range(j)]) + sum_ldl = L[j, :j] * v + d_j = M[j, j] - sum_ldl[0] + if d_j<0: + return False + d_diag[j] = d_j + if d_j.is_zero(): + for i in range(j + 1, n): + s_ij = sum(L[i, k] * L[j, k] * d_diag[k] for k in range(j)) + if not (M[i, j] - s_ij).is_zero(): + return False + else: + for i in range(j + 1, n): + sum_ldl_ij = sum(L[i, k] * L[j, k] * d_diag[k] for k in range(j)) + L[i, j] = (M[i, j] - sum_ldl_ij) / d_j + return True + +def _min_symm_eig(M): + r""" + Returns the smallest eigenvalue of a symmetric matrix. + + Returns a numerical approximation using numpy, for debugging + purposes. + """ + import numpy as np + Mnp = M.n().numpy() + return min(np.linalg.eigh((Mnp.transpose() + Mnp)/2).eigenvalues) + +def _parse_sdpa_block_matrices(full_text, section_label, next_label): + r""" + Helper function to read sdpa output matrices. + """ + m = re.search(r'%s\s*=' % re.escape(section_label), full_text) + if not m: + return [] + start = m.end() + try: + brace_start = full_text.index('{', start) + except ValueError: + return [] + end = len(full_text) + if next_label is not None: + pos = full_text.find(next_label, brace_start) + if pos != -1: + end = pos + block_text = full_text[brace_start:end] + body = block_text.strip() + body2 = re.sub(r'}\s*\n\s*{', '},\n{', body) + body_py = body2.replace('{', '[').replace('}', ']') + blocks_raw = ast.literal_eval(body_py) + blocks = [] + for blk in blocks_raw: + if blk and isinstance(blk[0], (list, tuple)): + rows = [[float(str(x)) for x in row] for row in blk] + blocks.append(rows) + else: + diag = [float(str(x)) for x in blk] + blocks.append(diag) + return blocks + +def _parse_sdpa_qd_result(filename): + r""" + Reads the sdpa output and returns the solution as a python dictionary. + + The return dictionary contains entries for: + 'status', 'primal', 'dual', 'y', 'X', Z' + """ + with open(filename, 'r') as f: + txt = f.read() + m = re.search(r'phase\.value\s*=\s*([A-Za-z0-9_]+)', txt) + status = m.group(1) if m else None + def _get_scalar(name): + m = re.search( + r'%s\s*=\s*([+-]?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)' % re.escape(name), + txt + ) + return float(m.group(1)) if m else None + obj_primal = _get_scalar('objValPrimal') + obj_dual = _get_scalar('objValDual') + Z_blocks = _parse_sdpa_block_matrices(txt, 'xMat', 'yMat') + X_blocks = _parse_sdpa_block_matrices(txt, 'yMat', 'main loop time') + return { + 'status' : status, + 'primal' : obj_primal, + 'dual' : obj_dual, + 'y' : Z_blocks[-2], + 'X' : X_blocks, + 'Z' : Z_blocks, + } + +class _CombinatorialTheory(Parent, UniqueRepresentation): + def __init__(self, name): + r""" + A combinatorial theory (hereditary class) for flag algebra calculations. + + A :class:`CombinatorialTheory` is a callable object: calling it constructs an + (induced) :class:`~sage.algebras.flag.flag.Flag` in the theory. + + **Induced convention.** + When you build a flag, any relation not explicitly listed is assumed absent. + For graphs, this means ``G(3, edges=[[0,1]])`` is the induced 3-vertex graph + with exactly one edge. + + **Exclusion is hereditary.** + Excluded flags/patterns are forbidden as induced subflags in all larger flags + generated by the theory, and in all optimization problems built from it. + + Basic usage + ----------- + + :: + + sage: G = GraphTheory + sage: cherry = G(3, edges=[[0,1],[0,2]]) + sage: other = G(3, edges=[[0,1],[1,2]]) + sage: cherry == other + True + + Resetting and excluding + ----------------------- + + :meth:`reset` clears the excluded list; :meth:`exclude` appends to it. + + :: + + sage: G.reset() + sage: G.exclude(G(3)) # exclude empty triple + sage: G.exclude(G(2)) # also exclude empty edge + sage: G.reset() + + Generating flags + ---------------- + + :meth:`generate` enumerates canonical representatives (up to isomorphism) of + flags of a given size, respecting exclusions. If a type is given, it generates + typed flags compatible with that type. + + :: + + sage: G.reset() + sage: flags = G.generate(3) + sage: len(flags) > 0 + True + + Optimization + ------------ + + :meth:`optimize` builds and solves a flag-algebra SDP bound for the density of + a target expression. + + Key parameters: + + - ``target``: a flag or a flag-algebra element (linear combination of flags) + - ``N``: expansion size (largest flags used) + - ``maximize``: maximize (default) or minimize + - ``positives``: list of expressions assumed nonnegative + - ``exact`` / ``denom``: attempt rational (or ring) rounding + - ``construction``: provide known optimal constructions (for rounding / sanity) + - ``file``: save a certificate to disk + - ``ring``: perform rounding over an extended coefficient ring (if supported) + + Example (Mantel): + + :: + + sage: G = GraphTheory + sage: tri = G(3, edges=[[0,1],[0,2],[1,2]]) + sage: e = G(2, edges=[[0,1]]) + sage: G.reset(); G.exclude(tri) + sage: G.optimize(e, 3, exact=True) # not tested + + Export / verify + --------------- + + Use :meth:`external_optimize` to export an SDP instance and :meth:`verify` to + check saved certificates. + + .. SEEALSO:: + + :class:`~sage.algebras.flag.flag.Flag` + :class:`~sage.algebras.flag.algebra.FlagAlgebra` + :meth:`exclude`, :meth:`reset`, :meth:`generate` + :meth:`optimize`, :meth:`external_optimize`, :meth:`verify` + + """ + self._name = name + self._excluded = tuple() + self._printlevel = 1 + Parent.__init__(self, category=(Sets(), )) + self._populate_coercion_lists_() + + def _repr_(self): + r""" + Give a nice string representation of the theory object + + OUTPUT: The representation as a string + + EXAMPLES:: + + sage: print(GraphTheory) + Theory for Graph + """ + return 'Theory for {}'.format(self._name) + + def signature(self): + r""" + Returns the signature data for this theory + + OUTPUT: A dictionary containing the signature data + + EXAMPLES:: + + sage: GraphTheory.signature() + {'edges': {'arity': 2, 'group': 0, 'ordered': False}} + """ + return self._signature + + def symmetries(self): + r""" + Returns the symmetry data for this theory + """ + return self._symmetries + + def sizes(self): + return NN + + # Persistend data management + def _calcs_dir(self): + r""" + Returns the path where the calculations are stored. + + EXAMPLES:: + + sage: GraphTheory._calcs_dir() + '/home/bodnalev/.sage/calcs' + """ + calcs_dir = os.path.join(os.getenv('HOME'), '.sage', 'calcs') + if not os.path.exists(calcs_dir): + os.makedirs(calcs_dir) + return calcs_dir + + def _save(self, data, key=None, path=None, name=None): + r""" + Saves data to persistent storage. + + The file name is determined based on the provided arguments. If ``name`` is not + given, a hashed key is generated from ``(self, key)`` and appended to ``self._name``. + The file is saved in the directory given by ``path`` if provided, or in the directory + returned by ``self._calcs_dir()`` if ``path`` is None. If ``path`` is an empty string, + the file is saved in the current working directory. + + INPUT: + + - ``data`` -- any serializable object; the data to be saved. + - ``key`` -- optional; used to generate the file name if ``name`` is not provided. + - ``path`` -- optional; the directory path where the file will be saved. + - ``name`` -- optional; explicit file name. Overrides key-based file naming if provided. + + OUTPUT: + None + """ + if name == None: + if key == None: + raise ValueError("Either the key or the name must be provided!") + serialized_key = pickle.dumps((self, key)) + hashed_key = hashlib.sha256(serialized_key).hexdigest() + file_name = self._name + "." + hashed_key + else: + file_name = name + + if path == None: + file_path = os.path.join(self._calcs_dir(), file_name) + elif path == "": + file_path = file_name + else: + if not os.path.exists(path): + os.makedirs(path) + file_path = os.path.join(path, file_name) + save_object = {'key': key, 'data': data} + with open(file_path, "wb") as file: + pickle.dump(save_object, file) + + def _load(self, key=None, path=None, name=None): + r""" + Loads data from persistent storage. + + The file name is determined by the provided ``key`` or explicitly by ``name``. + If ``name`` is not provided, a hash is computed from ``(self, key)`` and appended to + ``self._name``. The file is then sought in the directory specified by ``path`` (if given) + or in the directory returned by ``self._calcs_dir()`` (if ``path`` is None). If the file + does not exist, the function returns ``None``. Additionally, if a key is provided and the + loaded data's key does not match the provided key, a warning is issued and ``None`` is returned. + + INPUT: + + - ``key`` -- optional; used to generate the file name if ``name`` is not provided. + - ``path`` -- optional; directory path where the file is expected to be. + - ``name`` -- optional; explicit file name. Overrides key-based file naming if provided. + + OUTPUT: + The data stored in the file if found and valid; otherwise, ``None``. + """ + if key != None: + serialized_key = pickle.dumps((self, key)) + hashed_key = hashlib.sha256(serialized_key).hexdigest() + file_name = self._name + "." + hashed_key + if name != None: + file_name = name + + if path == None: + file_path = os.path.join(self._calcs_dir(), file_name) + else: + if not os.path.exists(path): + os.makedirs(path) + file_path = os.path.join(path, file_name) + + if not os.path.exists(file_path): + return None + + with open(file_path, "rb") as file: + save_object = pickle.load(file) + + if key != None and save_object != None and save_object['key'] != key: + import warnings + warnings.warn("Hash collision or corrupted data!") + return None + + return save_object['data'] + + def show_files(self): + r""" + Shows the persistent files saved from this theory. + """ + for xx in os.listdir(self._calcs_dir()): + if xx.startswith(self._name + "."): + file_path = os.path.join(self._calcs_dir(), xx) + with open(file_path , "rb") as file: + data = pickle.load(file) + if data != None: + print(data["key"][:2]) + + def clear(self): + r""" + Clears all calculation from the persistent memory. + """ + for xx in os.listdir(self._calcs_dir()): + if xx.startswith(self._name + "."): + file_path = os.path.join(self._calcs_dir(), xx) + os.remove(file_path) + + def _serialize(self, excluded=None): + r""" + Serializes this theory. Note this contains information about + the structures excluded from this theory. + + EXAMPLES:: + + sage: GraphTheory._serialize() + {'excluded': ((3, (), ((0, 1), (0, 2), (1, 2))),), + 'name': 'Graph', + 'signature': {'edges': {'arity': 2, 'group': 0, 'ordered': False}}, + 'sources': None, + 'symmetries': ((1, 1, ()),)} + """ + if excluded==None: + excluded = self.get_total_excluded(100000) + else: + excluded = tuple(excluded) + sourceser = None + if self._sources != None: + sourceser = ( + self._sources[0]._serialize(), + self._sources[1]._serialize() + ) + return { + "name": self._name, + "signature": self._signature, + "symmetries": self._symmetries, + "sources": sourceser, + "excluded": tuple([xx._serialize() for xx in excluded]) + } + + def printlevel(self, val): + self._printlevel = val + + # Optimizing and rounding + + def blowup_construction(self, target_size, parts, **kwargs): + # + # Initial setup, parsing parameters, setting up args + # + from sage.all import get_coercion_model, PolynomialRing + from sage.all import multinomial_coefficients + + coercion = get_coercion_model() + base_field = QQ + if target_size < 0: + raise ValueError("Target size must be non-negative.") + + # Parsing the parts + part_vars_names = [] + part_weights_raw = [] + num_parts = 0 + if isinstance(parts, Integer): + num_parts = parts + if parts <= 0: + raise ValueError("Number of parts must be positive") + part_weights_raw = [1 / parts] * parts + elif isinstance(parts, (list, tuple)): + num_parts = len(parts) + for p_spec in parts: + part_weights_raw.append(p_spec) + if isinstance(p_spec, str): + part_vars_names.append(p_spec) + else: + base_field = coercion.common_parent( + base_field, p_spec.parent() + ) + else: + raise TypeError( + "The provided parts must be an integer or a list/tuple" + ) + + # Parsing the relations + _temp_prob_vars = set() + current_signature_map_for_scan = self.signature() + for rel_name, definition in kwargs.items(): + if rel_name not in current_signature_map_for_scan: + continue + if isinstance(definition, dict): + for _, prob_spec_scan in definition.items(): + if isinstance(prob_spec_scan, str): + _temp_prob_vars.add(prob_spec_scan) + else: + base_field = coercion.common_parent( + base_field, prob_spec_scan + ) + + # Create base ring + prob_vars_names_list = sorted(list(_temp_prob_vars)) + # For now, just merge the params, maybe throw error if there's overlap + all_var_names = sorted(list(set(part_vars_names + prob_vars_names_list))) + if all_var_names: + R = PolynomialRing(base_field, names=all_var_names) + var_map = { + name: R.gen(i) for i, name in enumerate(all_var_names) + } + else: + R = base_field + var_map = {} + part_weights_R = [ + var_map[p_raw] if isinstance(p_raw, str) else R(p_raw) + for p_raw in part_weights_raw + ] + + # Separate relations to deterministic and probabilistic + deterministic_blowup_relations_R = set() + probabilistic_blowup_relations_R = {} + current_signature_map = self.signature() + for rel_name, definition in kwargs.items(): + if rel_name not in current_signature_map: + continue + sig_details = current_signature_map[rel_name] + isordered = sig_details["ordered"] + if isinstance(definition, (list, tuple)): + for edge in definition: + if isordered: + key = (rel_name, tuple(edge)) + else: + key = (rel_name, tuple(sorted(edge))) + deterministic_blowup_relations_R.add(key) + elif isinstance(definition, dict): + for edge, prob in definition.items(): + if isordered: + key = (rel_name, tuple(edge)) + else: + key = (rel_name, tuple(sorted(edge))) + try: + prob_R_val = R(prob) + except: + prob_R_val = var_map[prob] + if prob_R_val == 1: + deterministic_blowup_relations_R.add(key) + elif prob_R_val != 0: + probabilistic_blowup_relations_R[key] = prob_R_val + else: + raise TypeError("Relations must be lists or dictionaries") + + # + # Main calculation + # + + # Helper to get probabilistic part + def calculate_contribution(verts_assignment): + vertex_indices = list(range(target_size)) + base_relations_for_this_outcome = {} + potential_probabilistic_specs = [] + + # Organize probabilistic relations + for rel_name_sig, sig_details in current_signature_map.items(): + arity_sig = sig_details["arity"] + is_ordered_sig = sig_details["ordered"] + if arity_sig > target_size: + continue + if is_ordered_sig: + vert_iterator = itertools.permutations(vertex_indices, arity_sig) + else: + vert_iterator = itertools.combinations(vertex_indices, arity_sig) + + for v_tuple in vert_iterator: + parts_v_tuple = tuple(verts_assignment[v_idx] for v_idx in v_tuple) + key_in_blowup_def = (rel_name_sig, parts_v_tuple) + if key_in_blowup_def in deterministic_blowup_relations_R: + if rel_name_sig not in base_relations_for_this_outcome: + base_relations_for_this_outcome[rel_name_sig] = [] + base_relations_for_this_outcome[rel_name_sig].append(list(v_tuple)) + elif key_in_blowup_def in probabilistic_blowup_relations_R: + prob_for_rel = probabilistic_blowup_relations_R[key_in_blowup_def] + potential_probabilistic_specs.append({ + "name": rel_name_sig, "verts": v_tuple, "prob": prob_for_rel + }) + + num_potential_probabilistic = len(potential_probabilistic_specs) + if num_potential_probabilistic == 0: + structure = self(target_size, **base_relations_for_this_outcome) + return structure.afae() + + ret_prob = 0 + for i in range(1 << num_potential_probabilistic): + current_outcome_prob_R = 1 + relations_for_this_specific_outcome = { + k: list(v) for k, v in base_relations_for_this_outcome.items() + } + for j in range(num_potential_probabilistic): + spec = potential_probabilistic_specs[j] + is_present_in_outcome = (i >> j) & 1 + if is_present_in_outcome: + current_outcome_prob_R *= spec["prob"] + rel_name, vert_list = spec["name"], list(spec["verts"]) + if rel_name not in relations_for_this_specific_outcome: + relations_for_this_specific_outcome[rel_name] = [] + relations_for_this_specific_outcome[rel_name].append(vert_list) + else: + current_outcome_prob_R *= (1 - spec["prob"]) + if current_outcome_prob_R == 0: + continue + structure = self(target_size, **relations_for_this_specific_outcome) + ret_prob += structure.afae() * current_outcome_prob_R + return ret_prob + + # Get blowup components + res = 0 + for exps_counts, mult_factor in multinomial_coefficients( + num_parts, target_size + ).items(): + verts_assignment_pattern = [ + part_idx for part_idx, count_in_part in enumerate(exps_counts) + for _ in range(count_in_part) + ] + weight_product_part_R = 1 + for i_part, count_in_part_val in enumerate(exps_counts): + if count_in_part_val > 0: + weight_product_part_R *= \ + (part_weights_R[i_part] ** count_in_part_val) + current_coeff_R = mult_factor * weight_product_part_R + term_contribution = calculate_contribution(verts_assignment_pattern) + res += term_contribution * current_coeff_R + return res + + def _check_matrix(self, M, ind): + if M==None: + return True + try: + if not M.is_positive_semidefinite(): + self.fprint("Rounded X matrix "+ + "{} is not semidefinite: {}".format( + ind, + _min_symm_eig(M) + )) + return False + except: + if not _custom_psd_test(M): + self.fprint("Rounded X matrix "+ + "{} is not semidefinite: {}".format( + ind, + _min_symm_eig(M) + )) + return False + return True + + def get_Z_matrices(self, phi_vector, table_constructor, test=True): + Zs = [] + for param in table_constructor.keys(): + ns, ftype, target_size = param + table = self.mul_project_table(ns, ns, ftype, ftype_inj=[], + target_size=target_size) + Zm = [None for _ in range(len(table_constructor[param]))] + for gg, morig in enumerate(table): + if phi_vector[gg]==0: + continue + for ii, base in enumerate(table_constructor[param]): + mat = base * morig * base.T + if Zm[ii]==None: + Zm[ii] = mat*phi_vector[gg] + else: + Zm[ii] += mat*phi_vector[gg] + Zs.append(Zm) + for jj, Zii in enumerate(Zm): + if test: + self._check_matrix(Zii, (param, jj)) + return Zs + + def _adjust_table_phi(self, table_constructor, phi_vectors_exact, + test=False): + r""" + Helper to modify a table constructor, incorporating extra data from + constructions (phi_vectors_exact) + """ + if len(phi_vectors_exact)==0: + return table_constructor + + to_pop = [] + for param in table_constructor.keys(): + ns, ftype, target_size = param + table = self.mul_project_table(ns, ns, ftype, ftype_inj=[], + target_size=target_size) + Zs = [ + [None for _ in range(len(phi_vectors_exact))] \ + for _ in range(len(table_constructor[param])) + ] + for gg, morig in enumerate(table): + for ii, base in enumerate(table_constructor[param]): + mat = base * morig * base.T + for phind, phi_vector_exact in enumerate(phi_vectors_exact): + if Zs[ii][phind]==None: + Zs[ii][phind] = mat*phi_vector_exact[gg] + else: + Zs[ii][phind] += mat*phi_vector_exact[gg] + + new_bases = [] + for ii, Zgroup in enumerate(Zs): + Z = None + for jj, Zjj in enumerate(Zgroup): + if test: + self._check_matrix(Zjj, (param, ii, jj)) + if Z==None: + Z = Zjj + else: + Z.augment(Zjj) + Zk = Z.kernel() + Zkern = Zk.basis_matrix() + #print("removing kernel:\n", Z.image().basis_matrix()) + if Zkern.nrows()>0: + new_bases.append( + matrix(Zkern * table_constructor[param][ii], + sparse=True) + ) + if len(new_bases)!=0: + table_constructor[param] = new_bases + else: + to_pop.append(param) + + for param in to_pop: + table_constructor.pop(param, None) + return table_constructor + + def _tables_to_sdp_data(self, table_constructor, prev_data=None): + r""" + Helper to transform the data from the multiplication + tables to an SDP input + """ + if prev_data==None: + block_sizes = [] + target = [] + mat_inds = [] + mat_vals = [] + else: + block_sizes, target, mat_inds, mat_vals = prev_data + block_index = len(block_sizes) + 1 + for params in table_constructor.keys(): + ns, ftype, target_size = params + table = self.mul_project_table( + ns, ns, ftype, ftype_inj=[], target_size=target_size + ) + block_sizes += [base.nrows() for base in table_constructor[params]] + + #only loop through the table once + for gg, morig in enumerate(table): + #for each base change create the entries + for plus_index, base in enumerate(table_constructor[params]): + mm = base * morig * base.T + dd = mm._dict() + if len(dd)>0: + inds, values = zip(*mm._dict().items()) + iinds, jinds = zip(*inds) + for cc in range(len(iinds)): + if iinds[cc]>=jinds[cc]: + mat_inds.extend( + [gg+1, block_index + plus_index, + iinds[cc]+1, jinds[cc]+1] + ) + mat_vals.append(values[cc]) + block_index += len(table_constructor[params]) + return block_sizes, target, mat_inds, mat_vals + + def _constraints_to_sdp_data(self, constraints_data, prev_data=None): + r""" + Helper to transform the data from the constraints to an SDP input data + """ + if prev_data==None: + block_sizes = [] + target = [] + mat_inds = [] + mat_vals = [] + else: + block_sizes, target, mat_inds, mat_vals = prev_data + flag_num, constraints_vals, constraints_flags_vec, _, __ = \ + constraints_data + block_index = len(block_sizes) + 1 + constr_num = len(constraints_vals) + for ii in range(constr_num): + mat_inds.extend([0, block_index+1, 1+ii, 1+ii]) + mat_vals.append(constraints_vals[ii]) + + for gg in range(flag_num): + mat_inds.extend([gg+1, block_index, gg+1, gg+1]) + mat_vals.append(1) + for ii in range(constr_num): + mat_inds.extend([gg+1, block_index+1, ii+1, ii+1]) + mat_vals.append(constraints_flags_vec[ii][gg]) + block_sizes += [-flag_num, -constr_num] + return block_sizes, target, mat_inds, mat_vals + + def _target_to_sdp_data(self, target, prev_data=None): + r""" + Helper to transform the target to an SDP input data + """ + if prev_data==None: + return [], list(target), [], [] + prev_data[1] = list(target) + return prev_data + + def _get_relevant_ftypes(self, target_size): + r""" + Returns the ftypes useful for optimizing up to `target_size` + """ + plausible_sizes = list(range(1, target_size)) + ftype_pairs = [] + for fs, ns in itertools.combinations(plausible_sizes, r=int(2)): + if ns+ns-fs <= target_size: + kk = ns-fs + found = False + for ii, (bfs, bns) in enumerate(ftype_pairs): + if bns-bfs==kk: + found = True + if ns>bns: + ftype_pairs[ii]=(fs, ns) + break + if not found: + ftype_pairs.append((fs, ns)) + + ftype_data = [] + for fs, ns in ftype_pairs: + ftype_flags = self.generate_flags(fs) + ftypes = [flag.subflag([], ftype_points=list(range(fs))) \ + for flag in ftype_flags] + for xx in ftypes: + ftype_data.append((ns, xx, target_size)) + ftype_data.sort() + return ftype_data + + def _create_table_constructor(self, ftype_data, target_size): + r""" + Table constructor is a dictionary that holds the data to construct + all the multiplication tables. + + For each ftype and base change it provides the data to create + the multiplication table. Also pre-computes the multiplication + tables if they are not calculated yet. + """ + + sym_asym_mats = [ + self.sym_asym_bases(dat[0], dat[1]) for dat in ftype_data + ] + + table_constructor = {} + if self._printlevel>0: + iterator = tqdm(enumerate(ftype_data)) + pbar = iterator + else: + iterator = enumerate(ftype_data) + pbar = None + for ii, dat in iterator: + ns, ftype, target_size = dat + #pre-calculate the table here + table = self.mul_project_table( + ns, ns, ftype, ftype_inj=[], target_size=target_size + ) + if table==None: + pbar.set_description( + "{} ({}) had singular table!".format(ftype, ns) + ) + continue + sym_base, asym_base = sym_asym_mats[ii] + bases = [] + if sym_base.nrows()!=0: + bases.append(sym_base) + if asym_base.nrows()!=0: + bases.append(asym_base) + table_constructor[dat] = bases + if not (pbar is None): + pbar.set_description("Done with mult table for {}".format(ftype)) + return table_constructor + + def _create_constraints_data(self, positives, target_element, target_size): + r""" + Creates the data that holds the linear constraints + """ + + base_flags = self.generate_flags(target_size) + factor_flags = [] + + if positives == None: + positives_list_exact = [] + constraints_vals = [] + else: + positives_list_exact = [] + if self._printlevel>0: + iterator = tqdm(range(len(positives))) + pbar = iterator + else: + iterator = range(len(positives)) + pbar = None + for ii in iterator: + fv = positives[ii] + if isinstance(fv, ExoticFlag) or isinstance(fv, BuiltFlag): + continue + kf = fv.ftype().size() + nf = fv.size() + df = target_size - nf + kf + mult_table = self.mul_project_table( + nf, df, fv.ftype(), ftype_inj=[], target_size=target_size + ) + factor_flags.append(self.generate(df, fv.ftype())) + fvvals = fv.values() + m = matrix([vector(fvvals*mat) for mat in mult_table]) + positives_list_exact += list(m.T) + if not (pbar is None): + pbar.set_description( + "Done with positivity constraint {}".format(ii) + ) + constraints_vals = [0]*len(positives_list_exact) + + # The one vector is also calculated here and is a linear constraint + if target_element.ftype().size()==0: + one_vector = vector([1]*len(base_flags)) + else: + one_vector = (target_element.ftype().project()<<( + target_size - target_element.ftype().size() + )).values() + positives_list_exact.extend([one_vector, one_vector*(-1)]) + constraints_vals.extend([1, -1]) + + return len(base_flags), constraints_vals, \ + positives_list_exact, one_vector, factor_flags + + def _round_sdp_solution_no_phi(self, sdp_result, sdp_data, + table_constructor, constraints_data, + **params): + r""" + Rounds the SDP solution without adjusting the phi vector. + + This function processes an SDP solution by rounding its matrices and slack variables. + It performs the following steps: + + - Rounds each matrix in the SDP result using a specified denominator (default: 1024), + correcting for any negative eigenvalues. + - Flattens the rounded matrices and computes slack variables based on the provided + table constructor and constraints data. + - Determines the final result by scaling the slack variables with a one-vector extracted + from the constraints. + + INPUT: + + - ``sdp_result`` -- dictionary; + - ``sdp_data`` -- tuple; + - ``table_constructor`` -- dictionary; + - ``constraints_data`` -- tuple; + - ``**kwargs`` -- keyword arguments for rounding parameters: + * ``denom`` (integer, default: 1024): the denominator used for rounding. + """ + + import numpy as np + from numpy import linalg as LA + from sage.functions.other import ceil + from sage.all import coercion_model + + # set up parameters + denom = params.get("denom", 1024) + + #unpack variables + + block_sizes, target_list_exact, mat_inds, mat_vals = sdp_data + target_vector_exact = vector(target_list_exact) + flags_num, ___, positives_list_exact, _, __ = \ + constraints_data + positives_matrix_exact = matrix( + QQ, len(positives_list_exact), flags_num, positives_list_exact + ) + + # find the one_vector from the equality constraint + one_vector_exact = positives_matrix_exact.rows()[-2] + # remove the equality constraints + positives_matrix_exact = positives_matrix_exact[:-2, :] + + flags_num = -block_sizes[-2] # same as |F_n| + + X_matrices_approx = sdp_result['X'][:-2] + X_matrices_rounded = [] + self.fprint("Rounding X matrices") + if self._printlevel>0: + iterator = tqdm(X_matrices_approx) + else: + iterator = X_matrices_approx + for X in iterator: + Xr = _round_matrix(X, method=0, denom=denom) + Xnp = np.array(Xr) + eigenvalues, eigenvectors = LA.eig(Xnp) + emin = min(eigenvalues) + if emin<0: + eminr = ceil(-emin*denom)/denom + Xr = matrix(QQ, Xr) + \ + diagonal_matrix(QQ, [eminr]*len(X), sparse=True) + X_matrices_rounded.append(Xr) + X_matrices_flat = [ + vector(_flatten_matrix(X.rows(), doubled=False)) \ + for X in (X_matrices_rounded) + ] + + e_vector_approx = sdp_result['X'][-1][:-2] + e_vector_rounded = vector(QQ, + _round_list(e_vector_approx, force_pos=True, method=0, denom=denom) + ) + + phi_vector_approx = sdp_result['y'] + phi_vector_rounded = vector(QQ, + _round_list(phi_vector_approx, force_pos=True, method=0, denom=denom) + ) + + slacks = target_vector_exact - positives_matrix_exact.T*e_vector_rounded + block_index = 0 + self.fprint("Calculating resulting bound") + if self._printlevel > 0: + iterator = tqdm(table_constructor.keys()) + else: + iterator = table_constructor.keys() + for params in iterator: + ns, ftype, target_size = params + table = self.mul_project_table( + ns, ns, ftype, ftype_inj=[], target_size=target_size + ) + for gg, morig in enumerate(table): + for plus_index, base in enumerate(table_constructor[params]): + block_dim = block_sizes[block_index + plus_index] + X_flat = X_matrices_flat[block_index + plus_index] + M = base * morig * base.T + M_flat_vector_exact = vector( + _flatten_matrix(M.rows(), doubled=True) + ) + prod = M_flat_vector_exact*X_flat + slacks = slacks - vector(prod.parent(), len(slacks), {gg:prod}) + block_index += len(table_constructor[params]) + # scale back slacks with the one vector, the minimum is the final result + result = min( + [slacks[ii]/oveii for ii, oveii in \ + enumerate(one_vector_exact) if oveii!=0] + ) + # pad the slacks, so it is all positive where it counts + slacks -= result*one_vector_exact + + self.fprint("The rounded result is {}".format(result)) + + return result, X_matrices_rounded, e_vector_rounded, \ + slacks, [phi_vector_rounded] + + def _round_sdp_solution_no_phi_alter(self, sdp_result, sdp_data, + table_constructor, constraints_data, + **params): + r""" + Round the SDP results output to get something exact. + """ + import gc + import time + + # set up parameters + denom = params.get("denom", 1024) + slack_threshold = params.get("slack_threshold", 1e-9) + linear_threshold = params.get("linear_threshold", 1e-6) + kernel_threshold = params.get("kernel_threshold", 1e-4) + kernel_denom = params.get("kernel_denom", 1024) + + # unpack variables + block_sizes, target_list_exact, _, __ = sdp_data + target_vector_exact = vector(target_list_exact) + flags_num, _, positives_list_exact, __, ___ = constraints_data + _ = None; __ = None; ___ = None; gc.collect() + positives_matrix_exact = matrix( + len(positives_list_exact), flags_num, positives_list_exact + ) + + # find the one_vector from the equality constraint + one_vector_exact = positives_matrix_exact.rows()[-2] + # remove the equality constraints + positives_matrix_exact = positives_matrix_exact[:-2, :] + + # dim: |F_n|, c vector, primal slack for flags + c_vector_approx = vector(sdp_result['X'][-2]) + + c_zero_inds = [ + FF for FF, xx in enumerate(c_vector_approx) if + (xx<=slack_threshold) + ] + + # same as m, number of positive constraints (-2 for the equality) + positives_num = -block_sizes[-1] - 2 + # dim: m, the e vector, primal slack for positivitives + e_vector_approx = vector(sdp_result['X'][-1][:-2]) + # as above but rounded + e_vector_rounded = vector(QQ, + _round_list(e_vector_approx, method=0, denom=denom) + ) + + # The f (ff) positivity constraints where the e vector is zero/nonzero + e_zero_inds = [ + ff for ff, xx in enumerate(e_vector_approx) if \ + (xx0 and min(e_nonzero_vector_corr)<0: + self.fprint("Linear coefficient is negative: {}".format( + min(e_nonzero_vector_corr) + )) + e_nonzero_vector_corr = [max(xx, 0) for xx in e_nonzero_vector_corr] + e_vector_dict = dict(zip(e_nonzero_inds, e_nonzero_vector_corr)) + e_vector_base = vector(e_vector_dict.values()).base_ring() + e_vector_corr = vector(e_vector_base, positives_num, e_vector_dict) + self.fprint("This took {}s".format(time.time() - start_time)) + start_time = time.time() + + X_final = [] + slacks = target_vector_exact - positives_matrix_exact.T*e_vector_corr + block_index = 0 + self.fprint("Calculating resulting bound") + if self._printlevel > 0: + iterator = tqdm(table_constructor.keys()) + else: + iterator = table_constructor.keys() + for params in iterator: + ns, ftype, target_size = params + table = self.mul_project_table( + ns, ns, ftype, ftype_inj=[], target_size=target_size + ) + for plus_index, base in enumerate(table_constructor[params]): + block_dim = X_sizes_corrected[block_index + plus_index] + X_ii_raw, x_vector_corr = _unflatten_matrix( + x_vector_corr, block_dim + ) + X_ii_raw = matrix(X_ii_raw) + recover_base = X_recover_bases[block_index + plus_index] + X_ii_small = recover_base * X_ii_raw * recover_base.T + + # verify semidefiniteness + if not self._check_matrix(X_ii_small, (params, plus_index)): + return None + + # update slacks + for gg, morig in enumerate(table): + M = base * morig * base.T + M_flat_vector_exact = vector( + _flatten_matrix(M.rows(), doubled=True) + ) + prod = M_flat_vector_exact*vector( + _flatten_matrix(X_ii_small.rows(), doubled=False) + ) + slacks = slacks - vector(prod.parent(), len(slacks), {gg:prod}) + + X_final.append(X_ii_small) + block_index += len(table_constructor[params]) + + # scale back slacks with the one vector, the minimum is the final result + result = min([slacks[ii]/oveii \ + for ii, oveii in enumerate(one_vector_exact) if \ + oveii!=0]) + slacks -= result*one_vector_exact + # pad the slacks, so it is all positive where it counts + self.fprint("This took {}s".format(time.time() - start_time)) + start_time = time.time() + + return result, X_final, e_vector_corr, slacks, [] + + def _round_sdp_solution_phi(self, sdp_result, sdp_data, + table_constructor, constraints_data, + phi_vectors_exact, **params): + r""" + Round the SDP results output to get something exact. + """ + import gc + import time + + # set up parameters + denom = params.get("denom", 1024) + slack_threshold = params.get("slack_threshold", 1e-9) + linear_threshold = params.get("linear_threshold", 1e-6) + kernel_threshold = params.get("kernel_threshold", 1e-4) + kernel_denom = params.get("kernel_denom", 1024) + + # unpack variables + block_sizes, target_list_exact, _, __ = sdp_data + target_vector_exact = vector(target_list_exact) + flags_num, _, positives_list_exact, __, ___ = constraints_data + _ = None; __ = None; ___ = None; gc.collect() + positives_matrix_exact = matrix( + len(positives_list_exact), flags_num, positives_list_exact + ) + + no_constr = len(phi_vectors_exact)==0 + phi_vector_exact = vector( + [0]*positives_matrix_exact.ncols() + ) if no_constr else phi_vectors_exact[0] + + # find the one_vector from the equality constraint + one_vector_exact = positives_matrix_exact.rows()[-2] + # remove the equality constraints + positives_matrix_exact = positives_matrix_exact[:-2, :] + + # dim: |F_n|, c vector, primal slack for flags + c_vector_approx = vector(sdp_result['X'][-2]) + + c_zero_inds = [ + FF for FF, xx in enumerate(c_vector_approx) if + (abs(xx)<=slack_threshold or phi_vector_exact[FF]!=0) + ] + + # same as m, number of positive constraints (-2 for the equality) + positives_num = -block_sizes[-1] - 2 + + # dim: m, witness that phi is positive + phi_pos_vector_exact = positives_matrix_exact*phi_vector_exact + + # dim: m, the e vector, primal slack for positivitives + e_vector_approx = vector(sdp_result['X'][-1][:-2]) + # as above but rounded + e_vector_rounded = vector(QQ, + _round_list(e_vector_approx, method=0, denom=denom) + ) + + # The f (ff) positivity constraints where the e vector is zero/nonzero + e_zero_inds = [ + ff for ff, xx in enumerate(e_vector_approx) if \ + (abs(xx)0 and min(e_nonzero_vector_corr)<0: + self.fprint("Linear coefficient is negative: {}".format( + min(e_nonzero_vector_corr) + )) + e_nonzero_vector_corr = [max(xx, 0) for xx in e_nonzero_vector_corr] + e_vector_dict = dict(zip(e_nonzero_inds, e_nonzero_vector_corr)) + e_vector_base = vector(e_vector_dict.values()).base_ring() + e_vector_corr = vector(e_vector_base, positives_num, e_vector_dict) + self.fprint("This took {}s".format(time.time() - start_time)) + start_time = time.time() + + X_final = [] + slacks = target_vector_exact - positives_matrix_exact.T*e_vector_corr + block_index = 0 + self.fprint("Calculating resulting bound") + if self._printlevel > 0: + iterator = tqdm(table_constructor.keys()) + else: + iterator = table_constructor.keys() + for params in iterator: + ns, ftype, target_size = params + table = self.mul_project_table( + ns, ns, ftype, ftype_inj=[], target_size=target_size + ) + for plus_index, base in enumerate(table_constructor[params]): + block_dim = X_sizes_corrected[block_index + plus_index] + X_ii_raw, x_vector_corr = _unflatten_matrix( + x_vector_corr, block_dim + ) + X_ii_raw = matrix(X_ii_raw) + recover_base = X_recover_bases[block_index + plus_index] + X_ii_small = recover_base * X_ii_raw * recover_base.T + + # verify semidefiniteness + if not self._check_matrix(X_ii_small, (params, plus_index)): + return None + + # update slacks + for gg, morig in enumerate(table): + M = base * morig * base.T + M_flat_vector_exact = vector( + _flatten_matrix(M.rows(), doubled=True) + ) + prod = M_flat_vector_exact*vector( + _flatten_matrix(X_ii_small.rows(), doubled=False) + ) + slacks = slacks - vector(prod.parent(), len(slacks), {gg:prod}) + + X_final.append(X_ii_small) + block_index += len(table_constructor[params]) + + # scale back slacks with the one vector, the minimum is the final result + result = min([slacks[ii]/oveii \ + for ii, oveii in enumerate(one_vector_exact) if \ + oveii!=0]) + # pad the slacks, so it is all positive where it counts + slacks -= result*one_vector_exact + self.fprint("This took {}s".format(time.time() - start_time)) + start_time = time.time() + + return result, X_final, e_vector_corr, slacks, phi_vectors_exact + + def _fix_X_bases(self, table_constructor, X_original): + r""" + Transforms the X matrices to a base that agrees with the original + list of flags + + Basically undoes the sym/asym changes and the reductions by the + constructions' kernels. + """ + from fractions import Fraction + if X_original==None: + return None + X_flats = [] + block_index = 0 + for params in table_constructor.keys(): + X_ii = None + for plus_index, base in enumerate(table_constructor[params]): + if X_ii == None: + X_ii = base.T * X_original[block_index + plus_index] * base + else: + X_ii += base.T * X_original[block_index + plus_index] * base + block_index += len(table_constructor[params]) + X_flat = _flatten_matrix(X_ii.rows()) + if QQ.has_coerce_map_from(X_ii.base_ring()): + X_flat = [ + Fraction(int(num.numerator()), int(num.denominator())) for + num in X_flat + ] + elif X_ii.base_ring()==RDF or X_ii.base_ring()==RR: + X_flat = [float(num) for num in X_flat] + else: + X_flat = vector(X_flat) + X_flats.append(X_flat) + return X_flats + + def _format_optimizer_output(self, table_constructor, mult=1, + sdp_output=None, rounding_output=None, + file=None, target_size=0, **misc): + r""" + Formats the outputs to a nice certificate + + The result contains: the final bound, the X matrices, the linear + coefficients, the slacks, a guess or exact construction, the + list of base flags, the list of used (typed) flags + """ + + from fractions import Fraction + + typed_flags = {} + for params in table_constructor.keys(): + ns, ftype, _target_size = params + typed_flags[(ns, ftype._pythonize())] = [ + flg._pythonize() for flg in self.generate_flags(ns, ftype) + ] + if target_size==0: + raise ValueError("Empty type constraints!") + + base_flags = [ + flg._pythonize() for flg in self.generate_flags(target_size) + ] + + factor_flags = misc.get("factor_flags", None) + if factor_flags != None: + python_factor_flags = [ + [xx._pythonize() for xx in factors] for + factors in factor_flags + ] + else: + python_factor_flags = [] + + def pythonize(dim, data): + if data==None: + return None + if dim==0: + try: + num, denom = (QQ(data)).as_integer_ratio() + return Fraction(int(num), int(denom)) + except: + return data + return [pythonize(dim-1, xx) for xx in data] + + result = None + X_original = None + e_vector = None + slacks = None + phi_vecs = None + target = pythonize(1, misc.get("target", None)) + positives = pythonize(2, misc.get("positives", None)) + if positives!=None: + positives = positives[:-2] + maximize = mult==-1 + + if sdp_output!=None: + #these should be regular float (and stay like that) + result = sdp_output['primal'] + oresult = result + e_vector = sdp_output['X'][-1][:-2] + slacks = sdp_output['X'][-2] + phi_vecs = [sdp_output['y']] + #except this, but this is transformed back + X_original = [matrix(xx) for xx in sdp_output['X'][:-2]] + elif rounding_output!=None: + oresult, X_original, e_vector, slacks, phi_vecs = rounding_output + result = pythonize(0, oresult) + e_vector = pythonize(1, e_vector) + slacks = pythonize(1, slacks) + phi_vecs = pythonize(2, phi_vecs) + result *= int(mult) + oresult *= mult + Xs = self._fix_X_bases(table_constructor, X_original) + + cert_dict = {"result": result, + "X matrices": Xs, + "e vector": e_vector, + "slack vector": slacks, + "phi vectors": phi_vecs, + "base flags": base_flags, + "typed flags": typed_flags, + "target": target, + "positives": positives, + "factor flags": python_factor_flags, + "maximize": maximize, + "target size": int(target_size) + } + + if file!=None and file!="" and file!="notebook": + if not file.endswith(".pickle"): + file += ".pickle" + with open(file, "wb") as file_handle: + pickle.dump(cert_dict, file_handle) + if file=="notebook": + return cert_dict + return oresult + + def _handle_sdp_params(self, **params): + sdp_params=["axtol", "atytol", "objtol", "pinftol", "dinftol", "maxiter", + "minstepfrac", "maxstepfrac", "minstepp", "minstepd", "usexzgap", + "tweakgap", "affine", "printlevel", "perturbobj", "fastmode"] + + if "precision" in params and params["precision"]!=None: + precision = params["precision"] + if "axtol" not in params: + params["axtol"] = precision + if "atytol" not in params: + params["atytol"] = precision + if "objtol" not in params: + params["objtol"] = precision + if "pinftol" not in params: + params["pinftol"] = 1/precision + if "dinftol" not in params: + params["dinftol"] = 1/precision + if "minstepp" not in params: + params["minstepp"] = precision + if "minstepd" not in params: + params["minstepd"] = precision + + if self._printlevel != 1: + if params=={}: + params = {"printlevel": self._printlevel} + else: + if "printlevel" not in params: + params["printlevel"] = self._printlevel + if params!={}: + with open("param.csdp", "w") as paramsfile: + for key, value in params.items(): + if str(key) not in sdp_params: + continue + paramsfile.write(f"{key}={value}\n") + if "printlevel" in params: + self._printlevel = params["printlevel"] + else: + self._printlevel = 1 + + def solve_sdp(self, target_element, target_size, construction, + maximize=True, positives=None, file=None, + specific_ftype=None, solver="default", **params): + r""" + TODO Docstring + """ + from csdpy import solve_sdp + import time + + # + # Initial setup + # + + self._handle_sdp_params(**params) + base_flags = self.generate_flags(target_size) + self.fprint("Base flags generated, their number is {}".format( + len(base_flags) + )) + mult = -1 if maximize else 1 + target_vector_exact = ( + target_element.project()*(mult) << \ + (target_size - target_element.size()) + ).values() + sdp_data = self._target_to_sdp_data(target_vector_exact) + + # + # Create the relevant ftypes + # + + if specific_ftype==None: + ftype_data = self._get_relevant_ftypes(target_size) + else: + ftype_data = specific_ftype + self.fprint("The relevant ftypes are constructed, their " + + "number is {}".format(len(ftype_data))) + flags = [self.generate_flags(dat[0], dat[1]) for dat in ftype_data] + flag_sizes = [len(xx) for xx in flags] + self.fprint("Block sizes before symmetric/asymmetric change is" + + " applied: {}".format(flag_sizes)) + + # + # Create the table constructor and adjust it based on construction + # + + table_constructor = self._create_table_constructor( + ftype_data, target_size + ) + if isinstance(construction, FlagAlgebraElement): + phi_vectors_exact = [construction.values()] + else: + phi_vectors_exact = [xx.values() for xx in construction] + self.fprint("Adjusting table with kernels from construction") + table_constructor = self._adjust_table_phi( + table_constructor, phi_vectors_exact + ) + sdp_data = self._tables_to_sdp_data( + table_constructor, prev_data=sdp_data + ) + self.fprint("Tables finished") + + # + # Add constraints data and add it to sdp_data + # + + constraints_data = self._create_constraints_data( + positives, target_element, target_size + ) + sdp_data = self._constraints_to_sdp_data( + constraints_data, prev_data=sdp_data + ) + self.fprint("Constraints finished") + + # + # Then run the optimizer + # + + if solver=="default": + self.fprint("Running CSDP. Used block sizes are {}".format(sdp_data[0])) + time.sleep(float(0.1)) + final_sol = solve_sdp(*sdp_data) + time.sleep(float(0.1)) + elif solver.lower().endswith("sdpa_qd") or solver.lower().endswith("sdpa-qd"): + R = RealField(prec=256, sci_not=True) + with open("problem.dat-s", "w") as problem_file: + block_sizes, target, mat_inds, mat_vals = sdp_data + problem_file.write("{}\n{}\n".format(len(target), len(block_sizes))) + problem_file.write(" ".join(map(str, block_sizes)) + "\n") + for xx in target: + problem_file.write(str(R(xx)) + " ") + problem_file.write("\n") + for ii in range(len(mat_vals)): + problem_file.write("{} {} {} {} {}\n".format( + mat_inds[ii*4 + 0], + mat_inds[ii*4 + 1], + mat_inds[ii*4 + 2], + mat_inds[ii*4 + 3], + str(R(mat_vals[ii])) + )) + self.fprint("Running SDPA QD. Used block sizes are {}".format(sdp_data[0])) + commands = [solver, "-ds", "problem.dat-s", "-o", "sdpa.out"] + time.sleep(float(0.1)) + _process_result = subprocess.run(commands, check=True) + time.sleep(float(0.1)) + final_sol = _parse_sdpa_qd_result("sdpa.out") + os.remove("problem.dat-s") + else: + raise RuntimeError(f"Solver {solver} is not known.") + + if file!=None: + if not file.endswith(".pickle"): + file += ".pickle" + with open(file, "wb") as file_handle: + save_data = ( + final_sol, + (sdp_data[0], sdp_data[1], None, None), + table_constructor, + (constraints_data[0], None, constraints_data[2], None, constraints_data[4]), + phi_vectors_exact, + mult, target_size) + pickle.dump(save_data, file_handle) + + return final_sol["primal"]*mult + + def round_solution(self, sdp_output_file, certificate_file=None, **params): + r""" + TODO Docstring + """ + if not sdp_output_file.endswith(".pickle"): + sdp_output_file += ".pickle" + with open(sdp_output_file, "rb") as file_handle: + save_data = pickle.load(file_handle) + # + # Unpack the data + # + + sdp_result, sdp_data, table_constructor, \ + constraints_data, phi_vectors_exact, \ + mult, target_size = save_data + + + # + # Perform the rounding + # + + self.fprint("Starting the rounding of the result") + rounding_output = self._round_sdp_solution_phi( \ + sdp_result, + sdp_data, + table_constructor, + constraints_data, + phi_vectors_exact, + **params + ) + if rounding_output==None: + print("Rounding was unsuccessful!") + return + value = rounding_output[0]*mult + self.fprint("Final rounded bound is {}".format(value)) + + if certificate_file==None: + return value + return self._format_optimizer_output( + table_constructor, + mult=mult, + rounding_output=rounding_output, + file=certificate_file, + target=vector(sdp_data[1])*mult, + positives=constraints_data[2], + factor_flags=constraints_data[4], + target_size=target_size + ) + + def optimize_problem(self, target_element, target_size, maximize=True, + positives=None, construction=None, file=None, + exact=False, specific_ftype=None, + **params): + r""" + Try to maximize or minimize the value of `target_element` + + The algorithm calculates the multiplication tables and + sends the SDP problem to CSDPY. + + INPUT: + + - ``target_element`` -- Flag or FlagAlgebraElement; + the target whose density this function tries to + maximize or minimize in large structures. + - ``target_size`` -- integer; The program evaluates + flags and the relations between them up to this + size. + - ``maximize`` -- boolean (default: `True`); + - ``file`` -- file to save the certificate + (default: `None`); Use None to not create + certificate + - ``positives`` -- list of flag algebra elements, + optimizer will assume those are positive, can + have different types + - ``construction`` -- a list or a single element of + `FlagAlgebraElement`s; to consider in the kernel + `None` by default, in this case the construction + is guessed from a preliminary run. + - ``exact`` -- boolean; to round the result or not + + OUTPUT: A bound for the optimization problem. If + certificate is requested then returns the entire + output of the solver as the second argument. + """ + from csdpy import solve_sdp + import time + + # + # Initial setup + # + + self._handle_sdp_params(**params) + + constr_print_limit = params.get("constr_print_limit", 1000) + constr_error_threshold = params.get("constr_error_threshold", 1e-6) + + if target_size not in self.sizes(): + raise ValueError("For theory {}, size {} is not allowed.".format(self._name, target_size)) + + base_flags = self.generate_flags(target_size) + self.fprint("Base flags generated, their number is {}".format( + len(base_flags) + )) + mult = -1 if maximize else 1 + target_vector_exact = ( + target_element.project()*(mult) << \ + (target_size - target_element.size()) + ).values() + sdp_data = self._target_to_sdp_data(target_vector_exact) + + # + # Create the relevant ftypes + # + + if specific_ftype==None: + ftype_data = self._get_relevant_ftypes(target_size) + else: + ftype_data = specific_ftype + self.fprint("The relevant ftypes are constructed, their " + + "number is {}".format(len(ftype_data))) + flags = [self.generate_flags(dat[0], dat[1]) for dat in ftype_data] + flag_sizes = [len(xx) for xx in flags] + self.fprint("Block sizes before symmetric/asymmetric change is" + + " applied: {}".format(flag_sizes)) + + # + # Create the table constructor and add it to sdp_data + # + + table_constructor = self._create_table_constructor( + ftype_data, target_size + ) + sdp_data = self._tables_to_sdp_data( + table_constructor, prev_data=sdp_data + ) + self.fprint("Tables finished") + + # + # Add constraints data and add it to sdp_data + # + + constraints_data = self._create_constraints_data( + positives, target_element, target_size + ) + sdp_data = self._constraints_to_sdp_data( + constraints_data, prev_data=sdp_data + ) + self.fprint("Constraints finished") + + # + # Helper for returning the data, writing to file, and some cleanup + # + + def help_return(value, sdpo=None, roundo=None): + try: + os.remove("param.csdp") + except OSError: + pass + if file==None: + return value + return self._format_optimizer_output( + table_constructor, + mult=mult, + sdp_output=sdpo, + rounding_output=roundo, + file=file, + target=target_vector_exact*mult, + positives=constraints_data[2], + factor_flags=constraints_data[4], + target_size=target_size + ) + + # + # If construction is None or [] then run the optimizer + # without any construction + # + + if construction==None or construction==[] or construction is False: + self.fprint("Running sdp without construction. " + + "Used block sizes are {}".format(sdp_data[0])) + + time.sleep(float(0.1)) + initial_sol = solve_sdp(*sdp_data) + time.sleep(float(0.1)) + + # Format the result and return it if floating point values are fine + if (not exact): + return help_return(initial_sol['primal'] * mult, + sdpo=initial_sol) + + # Guess the construction in this case + if construction==None: + one_vector = constraints_data[3] + phi_vector_rounded, error_coeff = _round_adaptive( + initial_sol['y'], one_vector + ) + if error_coeff0 and min(e_values)<0: + print("Solution is not valid!") + self.fprint("Linear constraint's coefficient is negative ", + min(e_values)) + return -1 + + X_flats = to_sage(2, certificate["X matrices"]) + self.fprint("Checking X matrices") + if self._printlevel > 0: + iterator = tqdm(enumerate(X_flats)) + else: + iterator = enumerate(X_flats) + for ii, Xf in iterator: + X = matrix(_unflatten_matrix(Xf)[0]) + if not self._check_matrix(X, ii): + print("Solution is not valid!") + return -1 + + self.fprint("Solution matrices are all positive semidefinite, " + + "linear coefficients are all non-negative") + + # + # Initial setup + # + + if target_size==None: + if "target size" in certificate: + target_size = to_sage(0, certificate["target size"]) + else: + raise ValueError("Target size must be specified "+ + "if it is not part of the certificate!") + maximize = maximize and certificate.get("maximize", True) + mult = -1 if maximize else 1 + base_flags = certificate["base flags"] + one_vector = vector([1]*len(base_flags)) + if target_element==None: + if "target" in certificate: + target_vector_exact = vector(to_sage(1, certificate["target"])) + else: + raise ValueError("Target must be specified "+ + "if it is not part of the certificate!") + else: + target_vector_exact = ( + target_element.project()<<\ + (target_size - target_element.size()) + ).values() + if not (target_element.ftype().afae()==1): + one_vector = ( + target_element.ftype().project()<<\ + (target_size - target_element.ftype().size()) + ).values() + target_vector_exact *= mult + ftype_data = list(certificate["typed flags"].keys()) + + # + # Create the semidefinite matrix data + # + + table_list = [] + self.fprint("Calculating multiplication tables") + if self._printlevel > 0: + iterator = tqdm(enumerate(ftype_data)) + else: + iterator = enumerate(ftype_data) + for ii, dat in iterator: + ns, ftype = dat + ftype = self._element_constructor_(ftype[0], ftype=ftype[1], **dict(ftype[2])) + #calculate the table + table = self.mul_project_table( + ns, ns, ftype, ftype_inj=[], target_size=target_size + ) + if table!=None: + table_list.append(table) + + # + # Create the data from linear constraints + # + + if positives != None: + positives_list_exact = [] + for ii, fv in enumerate(positives): + if isinstance(fv, BuiltFlag) or isinstance(fv, ExoticFlag): + continue + nf = fv.size() + df = target_size + fv.ftype().size() - nf + mult_table = self.mul_project_table( + nf, df, fv.ftype(), ftype_inj=[], target_size=target_size + ) + fvvals = fv.values() + m = matrix([vector(fvvals*mat) for mat in mult_table]) + positives_list_exact += list(m.T) + self.fprint("Done with positivity constraint {}".format(ii)) + positives_matrix_exact = matrix( + len(positives_list_exact), len(base_flags), + positives_list_exact + ) + e_values = vector(e_values[:len(positives_list_exact)]) + else: + if "positives" in certificate: + posls = to_sage(2, certificate["positives"]) + positives_matrix_exact = matrix(len(posls), len(base_flags), posls) + e_values = vector(e_values[:len(posls)]) + else: + positives_matrix_exact = matrix(0, len(base_flags), []) + e_values = vector(QQ, []) + + self.fprint("Done calculating linear constraints") + + # + # Calculate the bound the solution provides + # + + self.fprint("Calculating the bound provided by the certificate") + + slacks = target_vector_exact - positives_matrix_exact.T*e_values + + if self._printlevel > 0: + iterator = tqdm(enumerate(table_list)) + else: + iterator = enumerate(table_list) + for ii, table in iterator: + slvecdel = [] + for mat_gg in table: + mat_flat = vector( + _flatten_matrix(mat_gg.rows(), doubled=True) + ) + slvecdel.append(mat_flat * vector(X_flats[ii])) + slacks -= vector(slvecdel) + + result = min( + [slacks[ii]/oveii for ii, oveii in enumerate(one_vector) \ + if oveii!=0] + ) + result *= mult + print("The solution is valid, it proves "+ + "the bound {}".format(result)) + + return result + + verify = verify_certificate + + + # Generating flags + + def _find_ftypes(self, empstrs, ftype): + import multiprocessing as mp + pool = mp.Pool(mp.cpu_count()-1) + pares = pool.map(ftype.ftypes_inside, empstrs) + pool.close(); pool.join() + return tuple(itertools.chain.from_iterable(pares)) + + # Generating tables + + def sym_asym_bases(self, n, ftype=None): + r""" + Generate the change of base matrices for the symmetric + and the asymmetric subspaces + """ + + flags = self.generate_flags(n, ftype) + uniques = [] + sym_base = [] + asym_base = [] + + #Correct method + sym_base_lasts = [] + for xx in flags: + xxid = xx.unique(weak=True)[0] + if xxid not in uniques: + uniques.append(xxid) + sym_base.append(xx.afae()) + sym_base_lasts.append(xx.afae()) + else: + sym_ind = uniques.index(xxid) + asym_base.append(sym_base_lasts[sym_ind] - xx) + sym_base[sym_ind] += xx + sym_base_lasts[sym_ind] = xx + + #Old worse method (but sometimes helps rounding) + # for xx in flags: + # xxid = xx.unique(weak=True)[0] + # if xxid not in uniques: + # uniques.append(xxid) + # sym_base.append(xx.afae()) + # else: + # sym_ind = uniques.index(xxid) + # asym_base.append(sym_base[sym_ind] - xx.afae()) + # sym_base[sym_ind] += xx + + + m_sym = matrix( + len(sym_base), len(flags), + [xx.values() for xx in sym_base], sparse=True + ) + m_asym = matrix( + len(asym_base), len(flags), + [xx.values() for xx in asym_base], sparse=True + ) + return m_sym, m_asym + + def _density_wrapper(self, ar): + r""" + Helper function used in the parallelization of calculating densities + """ + return ar[0].densities(*ar[1:]) + +class BuiltTheory(_CombinatorialTheory): + + Element = BuiltFlag + + def __init__(self, name, relation_name="edges", arity=2, + is_ordered=False, _from_data=None): + r""" + Initialize a Combinatorial Theory + + A combinatorial theory is any theory with universal axioms only, + (therefore the elements satisfy a heredetary property). + See the file docstring for more information. + + INPUT: + + - ``name`` -- string; name of the Theory + - ``relation_name`` -- string; name of the relation + - ``arity`` -- integer; arity of the relation + - ``is_ordered`` -- boolean; if the values are ordered + - ``_from_data`` -- list; only used internally + + OUTPUT: A CombinatorialTheory object + """ + + if _from_data != None: + self._sources = _from_data[0] + _from_data = _from_data[1] + + sered_signature = _from_data[0] + self._signature = {} + max_group = -1 + for ll in sered_signature: + key = ll[0] + val = { + "arity": ll[1][0], + "ordered": ll[1][1], + "group": ll[1][2] + } + max_group = max(max_group, val["group"]) + self._signature[key] = val + self._symmetries = _from_data[1] + if len(self._symmetries) != max_group+1: + print(self._symmetries) + print(self._signature) + raise ValueError("Provided data has different symmetry " + + "set size than group number") + else: + if arity < 1 or (arity not in NN): + raise ValueError("Arity must be nonzero positive integer!") + self._signature = {relation_name: { + "arity": arity, + "ordered": is_ordered, + "group": 0 + }} + self._sources = None + self._symmetries = ((1, 1, tuple()), ) + self._no_question = True + _CombinatorialTheory.__init__(self, name) + + # Parent methods + + def _element_constructor_(self, n, **kwds): + r""" + Construct elements of this theory + + INPUT: + + - ``n`` -- the size of the flag + - ``**kwds`` -- can contain ftype_points, listing + the points that will form part of the ftype; + and can contain the blocks for each signature. + If they are not included, they are assumed to + be empty lists. + + OUTPUT: A Flag with the given parameters + + EXAMPLES:: + + Create an empty graph on 3 vertices :: + + sage: GraphTheory(3) + Flag on 3 points, ftype from () with edges=() + + Create an edge with one point marked as an ftype :: + + sage: GraphTheory(2, ftype_points=[0], edges=[[0, 1]]) + Flag on 2 points, ftype from (0,) with edges=(01) + + .. NOTE:: + + Different input parameters can result in equal objects, for + example the following two graphs are automorphic:: + sage: b1 = [[0, 1], [0, 2], [0, 4], [1, 3], [2, 4]] + sage: b2 = [[0, 4], [1, 2], [1, 3], [2, 3], [3, 4]] + sage: g1 = GraphTheory(5, edges=b1) + sage: g2 = GraphTheory(5, edges=b2) + sage: g1==g2 + True + + .. SEEALSO:: + + :func:`__init__` of :class:`Flag` + """ + + if isinstance(n, BuiltFlag) or isinstance(n, Pattern): + if n.parent()==self: + return n + n = n.as_pattern() + return self.pattern(n.size(), ftype=n.ftype_points(), + **n.as_pattern().blocks()) + + ftype_points = tuple() + if 'ftype_points' in kwds: + try: + ftype_points = tuple(kwds['ftype_points']) + except: + raise ValueError("The provided ftype_points must be iterable") + elif 'ftype' in kwds: + try: + ftype_points = tuple(kwds['ftype']) + except: + raise ValueError("The provided ftype must be iterable") + + blocks = {} + for xx in self._signature.keys(): + blocks[xx] = tuple() + unary = (self._signature[xx]["arity"]==1) + if xx in kwds: + try: + blocks[xx] = tuple(kwds[xx]) + except: + raise ValueError("The provided {} must be iterable".format(xx)) + if unary: + if len(blocks[xx])>0: + try: + tuple(blocks[xx][0]) + except: + blocks[xx] = tuple([[aa] for aa in blocks[xx]]) + + return self.element_class(self, n, ftype_points, **blocks) + + def empty_element(self): + r""" + Returns the empty element, ``n``=0 and no blocks + + OUTPUT: The empty element of the CombinatorialTheory + + EXAMPLES:: + + sage: GraphTheory.empty_element() + Ftype on 0 points with edges=() + + .. NOTE:: + This has an alias called :func:`empty` + Since the underlying vertex set (empty set) + is the same as the ftype point set, this is + an ftype + + .. SEEALSO:: + + :func:`empty` + """ + blocks = {} + for xx in self._signature: + blocks[xx] = tuple() + return self.element_class(self, 0, tuple(), **blocks) + + empty = empty_element + + def pattern(self, n, **kwds): + r""" + Construct patterns for this theory + + INPUT: + + - ``n`` -- the size of the flag + - ``**kwds`` -- can contain ftype_points, listing + the points that will form part of the ftype; + and can contain the blocks for each signature. + If they are not included, they are assumed to + be empty lists. Can also contain missing relations + for each signature entry. + + OUTPUT: A Pattern with the given parameters + + EXAMPLES:: + + Create a pattern on 3 vertices with one edge required + and one edge missing :: + + sage: GraphTheory(3, edges=[[0, 1]], edges_m=[[1, 2]]) + Flag on 3 points, ftype from () with edges=(01) + + .. NOTE:: + Also has alias :func:`P`, :func:`p`, :func:`Pattern` + + .. SEEALSO:: + + :func:`__init__` of :class:`Pattern` + """ + ftype_points = tuple() + if 'ftype_points' in kwds: + try: + ftype_points = tuple(kwds['ftype_points']) + except: + raise ValueError("The provided ftype_points must be iterable") + elif 'ftype' in kwds: + try: + ftype_points = tuple(kwds['ftype']) + except: + raise ValueError("The provided ftype must be iterable") + if len(ftype_points)==n: + return self._element_constructor_(n, **kwds) + + blocks = {} + for xx in self._signature.keys(): + blocks[xx] = tuple() + blocks[xx+"_m"] = tuple() + unary = self._signature[xx]["arity"]==1 + + + if xx in kwds: + try: + blocks[xx] = tuple(kwds[xx]) + except: + raise ValueError( + "The provided {} must be iterable".format(xx) + ) + if unary: + if len(blocks[xx])>0: + try: + tuple(blocks[xx][0]) + except: + blocks[xx] = tuple([[aa] for aa in blocks[xx]]) + + for xx_missing in [xx+"_m", xx+"_missing", xx+"_miss"]: + if xx_missing in kwds: + blocks[xx+"_m"] = kwds[xx_missing] + + + try: + blocks[xx+"_m"] = tuple(kwds[xx_missing]) + except: + raise ValueError( + "The provided {} must be iterable".format(xx_missing) + ) + if unary: + if len(blocks[xx])>0: + try: + tuple(blocks[xx][0]) + except: + blocks[xx+"_m"] = tuple( + [[aa] for aa in blocks[xx+"_m"]] + ) + return Pattern(self, n, ftype_points, **blocks) + + p = pattern + P = pattern + Pattern = pattern + + def _an_element_(self, n=0, ftype=None): + r""" + Returns a random element + + INPUT: + + - ``n`` -- integer (default: `0`); size of the element + - ``ftype`` -- Flag (default: `None`); ftype of the element + if not provided then returns an element with empty ftype + + OUTPUT: A Flag with matching parameters + + EXAMPLES:: + + sage: GraphTheory._an_element_() + Ftype on 0 points with edges=() + """ + if ftype==None: + ftype = self.empty_element() + if n==None or n==ftype.size(): + return ftype + ls = self.generate_flags(n, ftype) + return ls[randint(0, len(ls)-1)] + + def some_elements(self): + r""" + Returns a list of elements + + EXAMPLES:: + + sage: GraphTheory.some_elements() + [Ftype on 0 points with edges=()] + """ + res = [self._an_element_()] + return res + + # Optimizing and rounding + + def _get_relevant_ftypes(self, target_size): + r""" + Returns the relevant ftypes for optimizing up to ``target_size``. + + INPUT: + + - ``target_size`` -- integer; the target size for which the ftypes are generated. + + OUTPUT: + list; a list of tuples where each tuple is of the form (ns, ftype, target_size). + + EXAMPLES: + + sage: GraphTheory._get_relevant_ftypes(3) + [(2, Ftype on 1 points with edges=(), 3)] + + TESTS: + + sage: GraphTheory._get_relevant_ftypes(-1) + Traceback (most recent call last): + ... + ValueError: Target size should be non-negative! + """ + if target_size < 0: + raise ValueError("Target size should be non-negative!") + + plausible_sizes = list(range(1, target_size)) + ftype_pairs = [] + for fs, ns in itertools.combinations(plausible_sizes, r=int(2)): + if ns + ns - fs <= target_size: + kk = ns - fs + found = False + for ii, (bfs, bns) in enumerate(ftype_pairs): + if bns - bfs == kk: + found = True + if ns > bns: + ftype_pairs[ii] = (fs, ns) + break + if not found: + ftype_pairs.append((fs, ns)) + + ftype_data = [] + for fs, ns in ftype_pairs: + ftype_flags = self.generate_flags(fs) + ftypes = [flag.subflag([], ftype_points=list(range(fs))) + for flag in ftype_flags] + for xx in ftypes: + ftype_data.append((ns, xx, target_size)) + ftype_data.sort() + return ftype_data + + # Generating flags + + def _guess_number(self, n): + if n==0: + return 1 + excluded = self.get_total_excluded(n) + key = ("generate", n, + self._serialize(excluded), self.empty()._serialize()) + loaded = self._load(key=key) + if loaded != None: + return len(loaded) + if self._sources==None: + max_arity = -1 + for xx in self._signature: + max_arity = max(max_arity, self._signature[xx]["arity"]) + if max_arity==1 or n=target_size: + break + if fs==0: + continue + plausible_sizes.append(fs) + ftype_pairs = [] + for fs, ns in itertools.combinations(plausible_sizes, r=int(2)): + if self.size_combine(fs, ns, ns) <= target_size: + kk = ns-fs + found = False + for ii, (bfs, bns) in enumerate(ftype_pairs): + if bns-bfs==kk: + found = True + if ns>bns: + ftype_pairs[ii]=(fs, ns) + break + if not found: + ftype_pairs.append((fs, ns)) + + ftype_data = [] + for fs, ns in ftype_pairs: + ftype_flags = self.generate_flags(fs) + ftypes = [flag.subflag([], ftype_points=list(range(fs))) for flag in ftype_flags] + for xx in ftypes: + ftype_data.append((ns, xx, target_size)) + ftype_data.sort() + return ftype_data + + # Generating flags + + def sizes(self): + return self._sizes + + def size_combine(self, k, n1, n2): + if k<0 or n1<0 or n2<0: + raise ValueError("Can't have negative size.") + if n1