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