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..c3cf6eae617 --- /dev/null +++ b/flag_tutorial.ipynb @@ -0,0 +1,3295 @@ +{ + "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, 560.44it/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: 6.40e-16 \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: 6.70e-16 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.06e-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.11e-15 0.00e+00 3.20e-09 0.00e+00 1.08e-09 3.47e-09\n", + "Starting the rounding of the result\n", + "Rounding X matrices\n", + "Calculating resulting bound\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████████████████████████████████| 1/1 [00:00<00:00, 377.15it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "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, 325.92it/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.5639371e-01 \n", + "Iter: 3 Ap: 1.00e+00 Pobj: -3.6548572e+00 Ad: 9.22e-01 Dobj: -2.8097530e-01 \n", + "Iter: 4 Ap: 1.00e+00 Pobj: -6.7102979e-01 Ad: 8.42e-01 Dobj: -3.1080769e-01 \n", + "Iter: 5 Ap: 1.00e+00 Pobj: -5.8576247e-01 Ad: 8.44e-01 Dobj: -4.8002387e-01 \n", + "Iter: 6 Ap: 1.00e+00 Pobj: -5.0647721e-01 Ad: 8.85e-01 Dobj: -4.9296240e-01 \n", + "Iter: 7 Ap: 1.00e+00 Pobj: -5.0060022e-01 Ad: 9.41e-01 Dobj: -4.9923793e-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: 4.18e-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: 4.38e-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.5000000069458577" + ] + }, + "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": 45, + "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": 45, + "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": 46, + "id": "35d47c34-9758-4fb7-9296-673703ca56f8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(False, False, False)" + ] + }, + "execution_count": 46, + "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": 49, + "id": "c9ae6813-815b-416d-a11d-0c99cba78393", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Ftype on 2 points with edges=(01)" + ] + }, + "execution_count": 49, + "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": 58, + "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": 58, + "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": 61, + "id": "2835afb0-55c7-44d6-9078-cd337c2ae629", + "metadata": {}, + "outputs": [], + "source": [ + "DiGraphTheory.exclude(DiGraphTheory(2, edges=[[0, 1], [1, 0]]))" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "id": "4d2d8a1d-3292-43f0-b76b-5e7ea30ac9de", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(Flag on 3 points, ftype from () with edges=(), Flag on 3 points, ftype from () with edges=(012))\n" + ] + }, + { + "data": { + "text/plain": [ + "Flag on 4 points, ftype from () with edges=(012 023 123)" + ] + }, + "execution_count": 62, + "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": 18, + "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": 19, + "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": 19, + "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": 63, + "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": 63, + "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": 65, + "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[65], line 1\u001b[0m\n\u001b[0;32m----> 1\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:194\u001b[0m, in \u001b[0;36mcombine\u001b[0;34m(name, symmetries, *theories)\u001b[0m\n\u001b[1;32m 192\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 193\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--> 194\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 195\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 196\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": [ + "combine(\"CombinedThing\", GraphTheory, ThreeGraphTheory)" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "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": 71, + "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": 72, + "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": 72, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "G.reset()\n", + "CombinedTheory.generate(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "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": 22, + "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": 73, + "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": 79, + "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": 80, + "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": 80, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "TT.generate(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "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": 81, + "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": 86, + "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": 86, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Graph(K4mSymmetry).plot()" + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "id": "ccd5c66e-aedf-4433-882b-38978fa2cd35", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Theory for Cyclic3ColorsOther" + ] + }, + "execution_count": 82, + "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": 27, + "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, 121.86it/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: 6.40e-16 \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: 6.70e-16 0.00e+00 2.46e-10 0.00e+00 2.59e-10 4.63e-10\n" + ] + }, + { + "data": { + "text/plain": [ + "0.5000000004275386" + ] + }, + "execution_count": 27, + "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": 28, + "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, 14.23it/s]" + ] + }, + { + "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" + ] + }, + { + "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.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: 1.34e-13 \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.80e-13 0.00e+00 4.23e-10 0.00e+00 1.08e-10 5.12e-10\n" + ] + }, + { + "data": { + "text/plain": [ + "0.6666666672726631" + ] + }, + "execution_count": 28, + "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": 29, + "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, 22.42it/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, 24.02it/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: 3.01e-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: 3.27e-15 0.00e+00 4.99e-09 0.00e+00 3.80e-10 3.51e-09\n" + ] + }, + { + "data": { + "text/plain": [ + "0.35355339235478905" + ] + }, + "execution_count": 29, + "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": 30, + "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, 204.10it/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, 7.49it/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" + ] + }, + { + "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.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.37e-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.49e-14 0.00e+00 3.95e-10 0.00e+00 3.78e-10 7.90e-10\n" + ] + }, + { + "data": { + "text/plain": [ + "0.2500000001939485" + ] + }, + "execution_count": 30, + "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": 31, + "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, 303.06it/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, 25.30it/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.90e-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: 2.75e-14 0.00e+00 5.35e-09 0.00e+00 1.62e-09 3.64e-09\n" + ] + }, + { + "data": { + "text/plain": [ + "0.5555555525754818" + ] + }, + "execution_count": 31, + "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": 32, + "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, 159.19it/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.5639371e-01 \n", + "Iter: 3 Ap: 1.00e+00 Pobj: -3.6548572e+00 Ad: 9.22e-01 Dobj: -2.8097530e-01 \n", + "Iter: 4 Ap: 1.00e+00 Pobj: -6.7102979e-01 Ad: 8.42e-01 Dobj: -3.1080769e-01 \n", + "Iter: 5 Ap: 1.00e+00 Pobj: -5.8576247e-01 Ad: 8.44e-01 Dobj: -4.8002387e-01 \n", + "Iter: 6 Ap: 1.00e+00 Pobj: -5.0647721e-01 Ad: 8.85e-01 Dobj: -4.9296240e-01 \n", + "Iter: 7 Ap: 1.00e+00 Pobj: -5.0060022e-01 Ad: 9.41e-01 Dobj: -4.9923793e-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: 4.18e-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: 4.38e-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: 4.18e-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: 4.38e-16 0.00e+00 3.20e-09 0.00e+00 1.08e-09 3.47e-09\n", + "Starting the rounding of the result\n", + "Rounding X matrices\n", + "Calculating resulting bound\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████████████████████████████████| 1/1 [00:00<00:00, 150.35it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Final rounded bound is 1/2\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/plain": [ + "1/2" + ] + }, + "execution_count": 32, + "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": 33, + "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, 255.93it/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, 196.38it/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", + "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.37e-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.49e-14 0.00e+00 3.95e-10 0.00e+00 3.78e-10 7.90e-10\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", + "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.35e-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.47e-14 0.00e+00 3.70e-10 0.00e+00 3.45e-10 7.40e-10\n", + "Starting the rounding of the result\n", + "Rounding X matrices\n", + "Calculating resulting bound\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████████████████████████████████| 2/2 [00:00<00:00, 139.46it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Final rounded bound is 1/4\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/plain": [ + "1/4" + ] + }, + "execution_count": 33, + "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": 34, + "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, 164.44it/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, 269.64it/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: 3.01e-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: 3.27e-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", + "Rounding X matrices\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████████████████████████████████| 4/4 [00:00<00:00, 481.05it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Calculating resulting bound\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████████████████████████████████| 2/2 [00:00<00:00, 322.99it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The rounded result is -363/1024\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/plain": [ + "363/1024" + ] + }, + "execution_count": 34, + "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": 35, + "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, 194.11it/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, 184.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", + "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: 3.01e-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: 3.27e-15 0.00e+00 4.99e-09 0.00e+00 3.80e-10 3.51e-09\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The initial run didn't provide an accurate construction\n", + "Rounding X matrices\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|█████████████████████████████████████| 4/4 [00:00<00:00, 1033.78it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Calculating resulting bound\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████████████████████████████████| 2/2 [00:00<00:00, 247.22it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The rounded result is -741457/2097152\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/plain": [ + "741457/2097152" + ] + }, + "execution_count": 35, + "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": 36, + "id": "bb54c471-cc89-4e1b-a3ff-c2e96291b828", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|████████████████████████████████████| 15/15 [00:00<00:00, 130.38it/s]\n", + "100%|███████████████████████████████████| 15/15 [00:00<00:00, 1287.68it/s]\n", + "100%|████████████████████████████████████| 15/15 [00:00<00:00, 195.67it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Base flags generated, their number is 10\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "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, 21.87it/s]\n" + ] + }, + { + "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", + "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.2226608e-01 \n", + "Iter: 3 Ap: 1.00e+00 Pobj: -9.0891397e+00 Ad: 8.85e-01 Dobj: -3.7904920e-01 \n", + "Iter: 4 Ap: 1.00e+00 Pobj: -2.2516642e+00 Ad: 7.96e-01 Dobj: -3.9403156e-01 \n", + "Iter: 5 Ap: 9.85e-01 Pobj: -7.9859298e-01 Ad: 8.64e-01 Dobj: -4.2692015e-01 \n", + "Iter: 6 Ap: 1.00e+00 Pobj: -7.9752891e-01 Ad: 8.05e-01 Dobj: -5.9421260e-01 \n", + "Iter: 7 Ap: 1.00e+00 Pobj: -6.7904726e-01 Ad: 8.09e-01 Dobj: -6.3478376e-01 \n", + "Iter: 8 Ap: 1.00e+00 Pobj: -6.6818656e-01 Ad: 8.96e-01 Dobj: -6.6029120e-01 \n", + "Iter: 9 Ap: 1.00e+00 Pobj: -6.6678401e-01 Ad: 1.00e+00 Dobj: -6.6632552e-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: 4.28e-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: 6.09e-15 0.00e+00 4.20e-09 0.00e+00 1.31e-09 3.42e-09\n", + "Starting the rounding of the result\n", + "Rounding X matrices\n", + "Linear coefficient is negative: -329/512\n", + "The result will be worse than expected.\n", + "Calculating resulting bound\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████████████████████████████████| 2/2 [00:00<00:00, 105.27it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Final rounded bound is 2/3\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/plain": [ + "2/3" + ] + }, + "execution_count": 36, + "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": 37, + "id": "0ca869fb-903b-40ed-92c4-fd9de7341bb1", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|████████████████████████████████████| 15/15 [00:00<00:00, 218.50it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Symbolic construction: \n", + " Flag Algebra Element over Multivariate Polynomial Ring in X0, X1, X2 over Rational Field\n", + "X0^4 + X1^4 + X2^4 - Flag on 4 points, ftype from () with edges=()\n", + "4*X0^3*X1 + 4*X0*X1^3 + 4*X0^3*X2 + 4*X1^3*X2 + 4*X0*X2^3 + 4*X1*X2^3 - Flag on 4 points, ftype from () with edges=(01 02 03)\n", + "6*X0^2*X1^2 + 6*X0^2*X2^2 + 6*X1^2*X2^2 - Flag on 4 points, ftype from () with edges=(02 03 12 13)\n", + "12*X0^2*X1*X2 + 12*X0*X1^2*X2 + 12*X0*X1*X2^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 X0, X1, X2 over Rational Field\n", + "8*X0^3 + 12*X0^2*X1 + 12*X0*X1^2 + 4*X1^3 - 12*X0^2 - 24*X0*X1 - 12*X1^2 + 12*X0 + 12*X1 - 4 - Flag on 4 points, ftype from () with edges=()\n", + "-32*X0^3 - 48*X0^2*X1 - 48*X0*X1^2 - 16*X1^3 + 48*X0^2 + 72*X0*X1 + 36*X1^2 - 24*X0 - 24*X1 + 4 - Flag on 4 points, ftype from () with edges=(01 02 03)\n", + "24*X0^3 + 36*X0^2*X1 + 36*X0*X1^2 + 12*X1^3 - 36*X0^2 - 24*X0*X1 - 12*X1^2 + 12*X0 - Flag on 4 points, ftype from () with edges=(02 03 12 13)\n", + "-24*X0*X1 - 12*X1^2 + 12*X1 - 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, 691.84it/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, 270.46it/s]\n" + ] + }, + { + "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", + "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: 1.31e-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: 1.89e-15 0.00e+00 3.74e-09 0.00e+00 7.87e-10 2.83e-09\n" + ] + }, + { + "data": { + "text/plain": [ + "0.5714285725860463" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# It is possible to create symbolic constructions\n", + "G.reset()\n", + "trip_constr = G.blowup_construction(4, 3, edges=[[0, 1], [0, 2], [1, 2]], symbolic=True)\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": 38, + "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, 505.22it/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.5639371e-01 \n", + "Iter: 3 Ap: 1.00e+00 Pobj: -3.6548572e+00 Ad: 9.22e-01 Dobj: -2.8097530e-01 \n", + "Iter: 4 Ap: 1.00e+00 Pobj: -6.7102979e-01 Ad: 8.42e-01 Dobj: -3.1080769e-01 \n", + "Iter: 5 Ap: 1.00e+00 Pobj: -5.8576247e-01 Ad: 8.44e-01 Dobj: -4.8002387e-01 \n", + "Iter: 6 Ap: 1.00e+00 Pobj: -5.0647721e-01 Ad: 8.85e-01 Dobj: -4.9296240e-01 \n", + "Iter: 7 Ap: 1.00e+00 Pobj: -5.0060022e-01 Ad: 9.41e-01 Dobj: -4.9923793e-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: 4.18e-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: 4.38e-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.5000000069458577" + ] + }, + "execution_count": 38, + "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": 39, + "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, 93.17it/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, 826.63it/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, 772.86it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The solution is valid, it proves the bound 29780781672501568/59561562885324537\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/plain": [ + "29780781672501568/59561562885324537" + ] + }, + "execution_count": 39, + "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", + "# For now, this transforms everything to a rational\n", + "G.reset()\n", + "G.exclude(G(3))\n", + "G.verify(\"test\", G(2), 3)" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "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, 206.43it/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, 22.33it/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": 41, + "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, 197.44it/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, 324.29it/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: 1.49e-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: 1.61e-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", + "Rounding X matrices\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████████████████████████████████| 4/4 [00:00<00:00, 942.70it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Calculating resulting bound\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████████████████████████████████| 2/2 [00:00<00:00, 238.29it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The rounded result is -363/1024\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/plain": [ + "363/1024" + ] + }, + "execution_count": 41, + "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": 42, + "id": "ed323bdd-46c1-461a-a7f3-fae76c1c050c", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "16it [00:00, 1226.27it/s]\n" + ] + }, + { + "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, 840.79it/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, 301.23it/s]\n" + ] + }, + { + "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", + "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: 2.19e-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: 2.37e-15 0.00e+00 5.18e-09 0.00e+00 3.25e-10 4.23e-09\n", + "Starting the rounding of the result\n", + "Rounding X matrices\n", + "Calculating resulting bound\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|███████████████████████████████████████| 2/2 [00:00<00:00, 98.88it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Final rounded bound is 1/4*sqrt2\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/plain": [ + "1/4*sqrt2" + ] + }, + "execution_count": 42, + "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", + "# Symbolic optimal construction \n", + "constr_symbolic = G.blowup_construction(4, 2, True, False, False, edges=[[0, 1], [1, 1]])\n", + "# Optimal construction in the extended field\n", + "constr = constr_symbolic.subs([1/sqrt(2), 1-(1/sqrt(2))], ring=TargetRing)\n", + "# Optimizer also in the extended field\n", + "G.optimize(G(3), 4, positives=[1/2 - G(2)], construction=constr, exact=True, ring=TargetRing)" + ] + }, + { + "cell_type": "markdown", + "id": "49e99676-293b-4d42-bfa7-3a70cb51d4dc", + "metadata": {}, + "source": [ + "Miscallenous\n", + "============" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "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": 44, + "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": 45, + "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": 46, + "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": 46, + "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.10.12" + } + }, + "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..4c9871a1327 --- /dev/null +++ b/src/sage/algebras/combinatorial_theory.py @@ -0,0 +1,4021 @@ +r""" +Implementation of flag algebras, with a class for combinatorial theories + + +A combinatorial theory is any theory with universal axioms only, +(therefore the elements satisfy a heredetary property). This +implementation allows the construction of any such theory, and +can perform flag algebraic computations on them. The theory of +flag algebras is from [Raz2007]_ + +To find out more about flags, how to create and manipulate them, +see :mod:`sage.algebras.flag`. This docstring is for combinatorial +theories and combinatorial optimization problems using flag algebras. + +The rest of this docstring will use `GraphTheory` since the number +of structures is realtively small there. `Flag` docstring shows +ways to create and calculate with flags. To create a flag we need to +provide the vertex size and a list of elements for each signature. +To create an edge flag for graphs, use :: + + sage: e = GraphTheory(2, edges=[[0, 1]]) + +To create a triangle `K_3` we can write :: + + sage: k3 = GraphTheory(3, edges=[[0, 1], [0, 2], [1, 2]]) + +If we have a theory, say GraphTheory, we can simply exclude structures +with :func:`exclude`. This takes a list of flags or a single flag :: + + sage: GraphTheory.exclude(k3) + +Excluding structures overwrites the theory, and in the future will only +consider members without the excluded structures. So with the above, +excluding a triangle will make GraphTheory not generate `k3`, or any larger +flag with induced `k3` in it. We can check this by generating flags of size +`4` :: + + sage: GraphTheory.generate(4) + (Flag on 4 points, ftype from () with edges=(), + Flag on 4 points, ftype from () with edges=(01), + Flag on 4 points, ftype from () with edges=(01 03), + Flag on 4 points, ftype from () with edges=(02 13), + Flag on 4 points, ftype from () with edges=(01 02 03), + Flag on 4 points, ftype from () with edges=(01 02 13), + Flag on 4 points, ftype from () with edges=(02 03 12 13)) + +Calling :func:`exclude` adds new structures to the list of flags we don't want +to generate, and we don't want to appear in larger strctures as an induced +subflag. To reset the theory and don't exclude anything, call :func:`reset`. + +To optimize the density of a flag (or linear combination of flags) in a theory, +we can call :func:`optimize`. To try to find the maximum number of +edges `e` in `k3` free graphs we can write :: + + sage: x = GraphTheory.optimize(e, 3) + ... + Success: SDP solved + ... + sage: abs(x-0.5)<1e-6 + True + +The second parameter, `optimize_problem(e, 3)` indicates the maximum size the +program expands the flags. We can reset the excluded graphs, and try to minimize +the density of triangles and empty triples :: + + sage: GraphTheory.reset() + sage: e3 = GraphTheory(3) + sage: x = GraphTheory.optimize(e3+k3, 3, maximize=False) + ... + sage: abs(x-0.25)<1e-6 + True + +The :func:`optimize` function requires csdpy, an sdp solver. But it is possible +to get these relations directly, by expressing the inequalities +as a sum of squares :: + + sage: GraphTheory.reset() + sage: GraphTheory.exclude(k3) + sage: pe = GraphTheory(2, ftype=[0]) - 1/2 + sage: 1/2 >= pe.mul_project(pe) * 2 + e + True + +:func:`mul_project` is short for multiplication and projection, the +non-negativity of self multiplication is preserved after projection so +`pe.mul_project(pe)` is non-negative on all large enough structures +giving that `1/2` is larger than `e` in all large enough structures. + +The following longer example shows that the density of K^3_4 is always +less than 3/8 in K^3_5-free hypergraphs. It uses the ThreeGraphTheory +object to deal with 3-uniform hypergraphs and hand-picked squares. +These values come from [Bod2023]_:: + + sage: em5 = ThreeGraphTheory(5, edges=[]) + sage: ThreeGraphTheory.exclude(em5) + + sage: em4 = ThreeGraphTheory(4, edges=[]) + + sage: em3p1 = ThreeGraphTheory(3, ftype_points=[0], edges=[]) + sage: sq1 = (em3p1 - 3/4).mul_project(em3p1 - 3/4) # todo: not implemented + + sage: la4p2 = ThreeGraphTheory(4, ftype_points=[0, 1], edges=[[0, 2, 3]]) + sage: lb4p2 = ThreeGraphTheory(4, ftype_points=[0, 1], edges=[[1, 2, 3]]) + sage: sq2 = (la4p2 - lb4p2).mul_project(la4p2 - lb4p2) # todo: not implemented + + sage: ma4p3 = ThreeGraphTheory(4, ftype_points=[0, 1, 2], edges=[[0, 1, 3]]) + sage: mb4p3 = ThreeGraphTheory(4, ftype_points=[0, 1, 2], edges=[[0, 2, 3]]) + sage: mc4p3 = ThreeGraphTheory(4, ftype_points=[0, 1, 2], edges=[[1, 2, 3]]) + sage: sq3 = (ma4p3 + mb4p3 + mc4p3 - 1/2).mul_project(ma4p3 + mb4p3 + mc4p3 - 1/2) # todo: not implemented + + sage: em4p3 = ThreeGraphTheory(4, ftype_points=[0, 1, 2]) + sage: sq4 = (em4p3 - 1/2).mul_project(em4p3 - 1/2) # todo: not implemented + + sage: n5q4 = ThreeGraphTheory(5, ftype_points=[0, 1, 2, 3], edges=[[0, 1, 2]]) + sage: sq5 = (n5q4 - 1/2).mul_project(n5q4 - 1/2) # todo: not implemented + + sage: oa5t4 = ThreeGraphTheory(5, ftype_points=[0, 1, 2, 3], edges=[[0, 1, 4]]) + sage: ob5t4 = ThreeGraphTheory(5, ftype_points=[0, 1, 2, 3], edges=[[2, 3, 4]]) + sage: sq6 = (oa5t4 - ob5t4).mul_project(oa5t4 - ob5t4) # todo: not implemented + + sage: sos = 2/3 * sq1 + 1/6 * sq2 + 13/12 * sq3 + 11/12 * sq4 + 2 * sq5 + 1/2 * sq6 # todo: not implemented + sage: 3/8 >= sos + em4 # todo: not implemented + True + + +.. SEEALSO:: + :func:`CombinatorialTheory.__init__` + :func:`CombinatorialTheory.exclude` + :func:`CombinatorialTheory.optimize_problem` + :func:`CombinatorialTheory.generate_flags` + +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 +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 functools import lru_cache + +import hashlib +import pickle +import os +from tqdm import tqdm + +# This should really be in the doctests +def test_generate(): + def test_theory(TT, nstart, nend, vals): + print("\nTesting {}".format(str(TT))) + for ii,jj in enumerate(range(nstart, nend+1)): + print("Size {}, the number is {} (should be {})".format( + jj, + len(TT.generate(jj)), + vals[ii] + )) + + CG = combine("CGraph", Color0, GraphTheory) + Cs = combine("Cs", Color0, Color1, symmetries=True) + CG.clear() + Cs.clear() + test_theory(Color0, 5, 10, [6, 7, 8, 9, 10, 11]) + test_theory(Cs, 3, 8, [13, 22, 34, 50, 70, 95]) + test_theory(GraphTheory, 3, 7, [4, 11, 34, 156, 1044]) + test_theory(ThreeGraphTheory, 3, 6, [2, 5, 34, 2136]) + test_theory(DiGraphTheory, 2, 5, [3, 16, 218, 9608]) + test_theory(DiThreeGraphTheory, 3, 3, [16]) + test_theory(CG, 2, 6, [6, 20, 90, 544, 5096]) + Cs.exclude([Cs(1), Cs(1, C0=[[0]], C1=[[0]])]) + GraphTheory.exclude(GraphTheory(3)) + CGp = combine("CGsym", GraphTheory, Cs) + CGp.clear() + test_theory(Cs, 4, 8, [3, 3, 4, 4, 5]) + test_theory(GraphTheory, 3, 8, [3, 7, 14, 38, 107, 410]) + test_theory(CGp, 2, 5, [4, 8, 32, 106]) + Css = combine("Colors3Sym", Color0, Color1, Color2, symmetries=True) + Css.clear() + pe = Css(1) + p0 = Css.p(1, C2=[0], C1=[0]) + Css.exclude([pe, p0]) + test_theory(Css, 3, 8, [3, 4, 5, 7, 8, 10]) + Cyc4 = combine("Cyclic4", Color0, Color1, Color2, Color3, + symmetries=CyclicSymmetry(4)) + Cyc4.clear() + Cyc4.exclude([ + Cyc4(1), + Cyc4.p(1, C0=[0], C1=[0]), + Cyc4.p(1, C0=[0], C2=[0]) + ]) + GraphTheory.reset() + T4 = combine("Cyclic4Graph", Cyc4, GraphTheory) + Cyc6 = combine("Cyclic6", Color0, Color1, Color2, Color3, Color4, Color5, + symmetries=CyclicSymmetry(6)) + Cyc6.clear() + Cyc6.exclude([ + Cyc6(1), + Cyc6.p(1, C0=[0], C1=[0]), + Cyc6.p(1, C0=[0], C2=[0]), + Cyc6.p(1, C0=[0], C3=[0]) + ]) + T6 = combine("Cyclic6Graph", Cyc6, GraphTheory) + T4.clear() + T6.clear() + test_theory(Cyc4, 4, 9, [10, 14, 22, 30, 43, 55]) + test_theory(Cyc6, 3, 7, [10, 22, 42, 80, 132]) + test_theory(T4, 2, 5, [6, 30, 260, 3052]) + test_theory(T6, 2, 4, [8, 62, 754]) + +def clear_all_calculations(theory_name=None): + calcs_dir = os.path.join(os.getenv('HOME'), '.sage', 'calcs') + if not os.path.exists(calcs_dir): + return + for xx in os.listdir(calcs_dir): + if theory_name==None or str(xx).startswith(theory_name): + file_path = os.path.join(calcs_dir, xx) + os.remove(file_path) + +def show_all_calculations(theory_name=None): + calcs_dir = os.path.join(os.getenv('HOME'), '.sage', 'calcs') + if not os.path.exists(calcs_dir): + return + for xx in os.listdir(calcs_dir): + file_path = os.path.join(calcs_dir, xx) + file_theory = str(xx).split(".")[0] + if theory_name==None: + with open(file_path , "rb") as file: + data = pickle.load(file) + if data != None: + print(file_theory + ": " + str(data["key"][:2])) + elif str(xx).startswith(theory_name): + with open(file_path , "rb") as file: + data = pickle.load(file) + if data != None: + print(data["key"][:2]) + +# 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): + 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] + 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(mat): + dim = mat.nrows() + for ii in range(dim): + pmin = mat[:ii+1, :ii+1] + if pmin.det()<0: + return False + return True + +class _CombinatorialTheory(Parent, UniqueRepresentation): + def __init__(self, name): + 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 + + EXAMPLES: + + sage: obj = SomeClass() # Assume SomeClass defines _name and _calcs_dir appropriately. + sage: obj._save("example data", key="sample") + sage: file_name = obj._name + "." + hashlib.sha256(pickle.dumps((obj, "sample"))).hexdigest() + sage: file_path = os.path.join(obj._calcs_dir(), file_name) + sage: os.path.exists(file_path) + True + + TESTS: + + sage: obj._save("data", name="test_file.pkl") + sage: os.path.exists("test_file.pkl") + True + + sage: obj._save("data") # Neither key nor name provided. + Traceback (most recent call last): + ... + ValueError: Either the key or the name must be provided! + """ + 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``. + + EXAMPLES: + + sage: obj = SomeClass() # Assume SomeClass defines _name and _calcs_dir appropriately. + sage: # Attempt to load data using a key. + sage: data = obj._load(key="sample_key") + sage: data # Should return the saved data or None if not found. + + TESTS: + + sage: # Test: loading a non-existent file returns None. + sage: obj._load(name="nonexistent_file.pkl") + None + + sage: # Test: key mismatch triggers a warning and returns None. + sage: obj._load(key="incorrect_key", name="existing_file.pkl") + Traceback (most recent call last): + ... + Warning: Hash collision or corrupted data! + """ + 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]) + } + + # Optimizing and rounding + + def blowup_construction(self, target_size, pattern_size, + symbolic_parts=False, symbolic=False, printlevel=None, + **kwargs): + + from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing + if symbolic: + symbolic_parts = True + import warnings + warnings.warn("The parameter symbolic will be replaced with symbolic_parts.", DeprecationWarning) + RX = PolynomialRing(QQ, pattern_size, "X") + Xs = RX.gens() + + flat_edges = [] + for kk in kwargs: + if kk not in self.signature(): + continue + for ee in kwargs[kk]: + flat_edges.append((kk, ee)) + + res = 0 + terms = ((sum(Xs))**target_size).dict() + if printlevel!=None: + self._printlevel = printlevel + if self._printlevel>0: + iterator = tqdm(terms) + else: + iterator = terms + for exps in iterator: + verts = [] + for ind, exp in enumerate(exps): + verts += [ind]*exp + coeff = terms[exps]/(pattern_size**target_size) + if symbolic_parts: + coeff = terms[exps] + for ind, exp in enumerate(exps): + coeff *= Xs[ind]**exp + blocks = {} + for rel in kwargs: + if rel not in self.signature(): + continue + reledges = kwargs[rel] + bladd = [] + for edge in reledges: + clusters = [ + [ii for ii in range(target_size) if verts[ii]==ee] \ + for ee in edge + ] + bladd += list( \ + set([tuple(sorted(xx)) \ + for xx in itertools.product(*clusters) \ + if len(set(xx))==len(edge)]) \ + ) + blocks[rel] = bladd + try: + res += self(target_size, **blocks).afae()*coeff + except: + raise ValueError( + "The construction contains excluded structures: ", + self(target_size, **blocks) + ) + return res + + 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 Zii in Zm: + if test and (not Zii.is_positive_semidefinite()): + self.fprint("Construction based Z matrix for " + + "{} is not semidef: {}".format( + ftype, min(Zii.eigenvalues()) + )) + 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 Zjj in Zgroup: + if test and (not Zjj.is_positive_semidefinite()): + self.fprint("Construction based Z matrix for " + + "{} is not semidef: {}".format( + ftype, min(Zjj.eigenvalues()) + )) + if Z==None: + Z = Zjj + else: + Z.augment(Zjj) + Zk = Z.kernel() + Zkern = Zk.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, one_vector = \ + 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) + + 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 + ) + fvvals = fv.values() + m = matrix(QQ, [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 + + 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. + + EXAMPLES: + + sage: + + TESTS: + + sage: + """ + + import numpy as np + from numpy import linalg as LA + from sage.functions.other import ceil + + # 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, constraints_vals, positives_list_exact, one_vector = \ + 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(QQ, + _flatten_matrix(M.rows(), doubled=True) + ) + slacks[gg] -= M_flat_vector_exact*X_flat + 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) + ring = params.get("ring", QQ) + 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(ring, target_list_exact) + flags_num, _, positives_list_exact, __ = constraints_data + _ = None; __ = None; gc.collect() + positives_matrix_exact = matrix( + ring, 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_corr = vector(ring, 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(ring, 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 X_ii_small.is_positive_semidefinite(): + self.fprint("Rounded X matrix "+ + "{} is not semidefinite: {}".format( + block_index+plus_index, + min(X_ii_small.eigenvalues()) + )) + 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) + ) + slacks[gg] -= M_flat_vector_exact*vector( + _flatten_matrix(X_ii_small.rows(), doubled=False) + ) + + 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, [] + + 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) + ring = params.get("ring", QQ) + 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(ring, target_list_exact) + flags_num, _, positives_list_exact, __ = constraints_data + _ = None; __ = None; gc.collect() + positives_matrix_exact = matrix( + ring, len(positives_list_exact), flags_num, positives_list_exact + ) + + no_constr = len(phi_vectors_exact)==0 + phi_vector_exact = vector( + ring, + [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_corr = vector(ring, 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(ring, 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 + invalid = False + try: + if not X_ii_small.is_positive_semidefinite(): + self.fprint("Rounded X matrix "+ + "{} is not semidefinite: {}".format( + block_index+plus_index, + min(X_ii_small.eigenvalues()) + )) + invalid = True + except: + if not custom_psd_test(X_ii_small): + self.fprint("Rounded X matrix "+ + "{} is not semidefinite: {}".format( + block_index+plus_index, + min(X_ii_small.eigenvalues()) + )) + invalid = True + if invalid: + 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) + ) + slacks[gg] -= M_flat_vector_exact*vector( + _flatten_matrix(X_ii_small.rows(), doubled=False) + ) + + 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. + """ + if X_original==None: + return None + X_flats = [] + block_index = 0 + for params in table_constructor.keys(): + ns, ftype, target_size = params + X_ii = None + for plus_index, base in enumerate(table_constructor[params]): + base_scale = base * base.T + 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_flats.append(vector(_flatten_matrix(X_ii.rows()))) + return X_flats + + def _format_optimizer_output(self, table_constructor, mult=1, + sdp_output=None, rounding_output=None, + file=None): + 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 + """ + + target_size = 0 + typed_flags = {} + for params in table_constructor.keys(): + ns, ftype, target_size = params + typed_flags[(ns, ftype)] = self.generate_flags(ns, ftype) + + base_flags = self.generate_flags(target_size) + + result = None + X_original = None + e_vector = None + slacks = None + phi_vecs = None + + if sdp_output!=None: + result = sdp_output['primal'] + X_original = [matrix(dat) for dat in sdp_output['X'][:-2]] + e_vector = vector(sdp_output['X'][-1]) + slacks = vector(sdp_output['X'][-2]) + phi_vecs = [vector(sdp_output['y'])] + elif rounding_output!=None: + result, X_original, e_vector, slacks, phi_vecs = rounding_output + + result *= mult + #Ps, Ls, Ds = self._fix_X_bases_pld(table_constructor, X_original) + 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 + } + + 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 result + + 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, **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 + # + + self.fprint("Running SDP. Used block sizes are {}".format(sdp_data[0])) + time.sleep(float(0.1)) + final_sol = solve_sdp(*sdp_data) + time.sleep(float(0.1)) + + 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), + phi_vectors_exact, + mult) + 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 = 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 + ) + + 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 + ) + + # + # 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[-1] + 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 = certificate["X matrices"] + # Ps = certificate["P dictionaries"] + # Ds = certificate["D vectors"] + # Ls = certificate["L 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(QQ, _unflatten_matrix(Xf)[0]) + if not (X.is_positive_semidefinite()): + print("Solution is not valid!") + self.fprint("Matrix {} is not semidefinite".format(ii)) + return -1 + # P = matrix(QQ, len(Ddiag), len(Ddiag), Ps[ii], sparse=True) + # D = diagonal_matrix(QQ, Ddiag, sparse=True) + # Larr, _ = _unflatten_matrix( + # Ls[ii], dim=len(Ddiag), + # doubled=False, upper=True + # ) + # L = matrix(QQ, Larr).T + # PL = P*L + # X = PL * D * PL.T + # X_flats.append(vector(QQ, _flatten_matrix(X.rows()))) + + self.fprint("Solution matrices are all positive semidefinite, " + + "linear coefficients are all non-negative") + + # + # Initial setup + # + + mult = -1 if maximize else 1 + base_flags = certificate["base flags"] + target_vector_exact = ( + target_element.project()*(mult)<<\ + (target_size - target_element.size()) + ).values() + 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() + + 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 + #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 + # + + positives_list_exact = [] + if positives != None: + 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(QQ, [vector(fvvals*mat) for mat in mult_table]) + positives_list_exact += list(m.T) + self.fprint("Done with positivity constraint {}".format(ii)) + else: + e_values = vector(QQ, 0) + positives_matrix_exact = matrix( + QQ, + len(positives_list_exact), len(base_flags), + positives_list_exact + ) + + 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: + for gg, mat_gg in enumerate(table): + mat_flat = vector( + _flatten_matrix(mat_gg.rows(), doubled=True) + ) + slacks[gg] -= mat_flat * X_flats[ii] + 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 = [] + 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 + 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) + asd + + TESTS: + + sage: GraphTheory._get_relevant_ftypes(-1) + asd + """ + 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