diff --git a/.gitignore b/.gitignore index 70a6739c33b..678ffbd48e8 100644 --- a/.gitignore +++ b/.gitignore @@ -225,6 +225,7 @@ build/pkgs/wheel/version_requirements.txt /pkgs/sagemath-sirocco/setup.cfg /pkgs/sagemath-tdlib/setup.cfg /pkgs/sagemath-categories/setup.cfg +/pkgs/sagemath-cmr/setup.cfg /pkgs/sagemath-environment/setup.cfg /pkgs/sagemath-repl/setup.cfg /pkgs/sagemath-objects/pyproject.toml @@ -235,6 +236,7 @@ build/pkgs/wheel/version_requirements.txt /pkgs/sagemath-sirocco/pyproject.toml /pkgs/sagemath-tdlib/pyproject.toml /pkgs/sagemath-categories/pyproject.toml +/pkgs/sagemath-cmr/pyproject.toml /pkgs/sagemath-environment/pyproject.toml /pkgs/sagemath-repl/pyproject.toml /pkgs/sagemath-objects/requirements*.txt @@ -245,6 +247,7 @@ build/pkgs/wheel/version_requirements.txt /pkgs/sagemath-sirocco/requirements*.txt /pkgs/sagemath-tdlib/requirements*.txt /pkgs/sagemath-categories/requirements*.txt +/pkgs/sagemath-cmr/requirements*.txt /pkgs/sagemath-environment/requirements*.txt /pkgs/sagemath-repl/requirements*.txt /pkgs/sagemath-categories/MANIFEST.in diff --git a/build/make/Makefile.in b/build/make/Makefile.in index 420dadb9364..ef805f682c6 100644 --- a/build/make/Makefile.in +++ b/build/make/Makefile.in @@ -142,6 +142,7 @@ PYPI_WHEEL_PACKAGES = $(PYPI_NOARCH_WHEEL_PACKAGES) \ sagemath_repl \ sagemath_categories \ sagemath_bliss \ + sagemath_cmr \ sagemath_mcqd \ sagemath_tdlib \ sagemath_coxeter3 \ diff --git a/build/pkgs/cmr/SPKG.rst b/build/pkgs/cmr/SPKG.rst new file mode 100644 index 00000000000..a8d73dd0f79 --- /dev/null +++ b/build/pkgs/cmr/SPKG.rst @@ -0,0 +1,32 @@ +cmr: Combinatorial matrix recognition +===================================== + +Description +----------- + +The following matrix classes can be recognized: + +- Totally Unimodular Matrices +- Network Matrices +- Complement Totally Unimodular Matrices +- (Strongly) k-Modular and Unimodular Matrices + +Moreover, representation matrices for the following matroid classes can be recognized: + +- Regular Matroids +- Graphic / Cographic / Planar Matrices +- Series-Parallel Matroids + + +License +------- + +MIT license + + +Upstream Contact +---------------- + +https://discopt.github.io/cmr/ + +https://github.com/discopt/cmr diff --git a/build/pkgs/cmr/checksums.ini b/build/pkgs/cmr/checksums.ini new file mode 100644 index 00000000000..9daba5c59cf --- /dev/null +++ b/build/pkgs/cmr/checksums.ini @@ -0,0 +1,4 @@ +tarball=cmr-0+VERSION.tar.gz +sha1=05a74a210ff6ed105595c6ccd4b70ce232dc5674 +sha256=3c4d7bfd4391a2e50491b1d259465892d4a040e3355a256b85090af6787272e4 +upstream_url=https://github.com/discopt/cmr/archive/VERSION.tar.gz diff --git a/build/pkgs/cmr/dependencies b/build/pkgs/cmr/dependencies new file mode 100644 index 00000000000..8e2118051ff --- /dev/null +++ b/build/pkgs/cmr/dependencies @@ -0,0 +1 @@ +googletest diff --git a/build/pkgs/cmr/dependencies_order_only b/build/pkgs/cmr/dependencies_order_only new file mode 100644 index 00000000000..a3ea3e4380f --- /dev/null +++ b/build/pkgs/cmr/dependencies_order_only @@ -0,0 +1 @@ +cmake diff --git a/build/pkgs/cmr/package-version.txt b/build/pkgs/cmr/package-version.txt new file mode 100644 index 00000000000..bcda2cd4917 --- /dev/null +++ b/build/pkgs/cmr/package-version.txt @@ -0,0 +1 @@ +e405fce633c4119a53249a5d0cade8a41fba081e diff --git a/build/pkgs/cmr/patches/0001-src-cmr-seymour_internal.h-Add-missing-include.patch b/build/pkgs/cmr/patches/0001-src-cmr-seymour_internal.h-Add-missing-include.patch new file mode 100644 index 00000000000..c99374e6b1f --- /dev/null +++ b/build/pkgs/cmr/patches/0001-src-cmr-seymour_internal.h-Add-missing-include.patch @@ -0,0 +1,25 @@ +From 72a78493855add45a1ec8ddcd9c1e4d1d08e7028 Mon Sep 17 00:00:00 2001 +From: Matthias Koeppe +Date: Sun, 13 Oct 2024 16:27:44 -0700 +Subject: [PATCH] src/cmr/seymour_internal.h: Add missing include + +--- + src/cmr/seymour_internal.h | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/src/cmr/seymour_internal.h b/src/cmr/seymour_internal.h +index 8abefea3..10fc4815 100644 +--- a/src/cmr/seymour_internal.h ++++ b/src/cmr/seymour_internal.h +@@ -1,6 +1,8 @@ + #ifndef CMR_SEYMOUR_INTERNAL_H + #define CMR_SEYMOUR_INTERNAL_H + ++#include ++ + #include "densematrix.h" + + #include +-- +2.42.0 + diff --git a/build/pkgs/cmr/spkg-check.in b/build/pkgs/cmr/spkg-check.in new file mode 100644 index 00000000000..d55199a4e91 --- /dev/null +++ b/build/pkgs/cmr/spkg-check.in @@ -0,0 +1,4 @@ +cd src +cd build +ctest +ctest --rerun-failed --output-on-failure diff --git a/build/pkgs/cmr/spkg-install.in b/build/pkgs/cmr/spkg-install.in new file mode 100644 index 00000000000..22ebcf53c9c --- /dev/null +++ b/build/pkgs/cmr/spkg-install.in @@ -0,0 +1,9 @@ +# -*- shell-script -*- +cd src +# Need C++14, so remove our flags that force C++11 +export CXX="$(echo "$CXX" | sed 's/-std=[a-z0-9+]*//g') -std=gnu++14" +mkdir build +cd build +sdh_cmake -DGENERATORS=on -DCMAKE_SYSTEM_PREFIX_PATH="$SAGE_LOCAL" -DHAVE_FLAG_SEARCH_PATHS_FIRST=0 -DSHARED=on .. +sdh_make VERBOSE=1 +sdh_make_install diff --git a/build/pkgs/cmr/type b/build/pkgs/cmr/type new file mode 100644 index 00000000000..134d9bc32d5 --- /dev/null +++ b/build/pkgs/cmr/type @@ -0,0 +1 @@ +optional diff --git a/build/pkgs/googletest/SPKG.rst b/build/pkgs/googletest/SPKG.rst new file mode 100644 index 00000000000..fde8c4ed840 --- /dev/null +++ b/build/pkgs/googletest/SPKG.rst @@ -0,0 +1,19 @@ +googletest: Google testing and mocking framework +================================================ + +Description +----------- + +C++ test framework + + +License +------- + +BSD-3-Clause license + + +Upstream Contact +---------------- + +https://github.com/google/googletest diff --git a/build/pkgs/googletest/checksums.ini b/build/pkgs/googletest/checksums.ini new file mode 100644 index 00000000000..f1855410d55 --- /dev/null +++ b/build/pkgs/googletest/checksums.ini @@ -0,0 +1,4 @@ +tarball=googletest-VERSION.tar.gz +sha1=568d58e26bd4e838449ca7ab8ebc152b3cbd210d +sha256=7b42b4d6ed48810c5362c265a17faebe90dc2373c885e5216439d37927f02926 +upstream_url=https://github.com/google/googletest/archive/refs/tags/vVERSION.tar.gz diff --git a/build/pkgs/googletest/dependencies_order_only b/build/pkgs/googletest/dependencies_order_only new file mode 100644 index 00000000000..a3ea3e4380f --- /dev/null +++ b/build/pkgs/googletest/dependencies_order_only @@ -0,0 +1 @@ +cmake diff --git a/build/pkgs/googletest/distros/alpine.txt b/build/pkgs/googletest/distros/alpine.txt new file mode 100644 index 00000000000..2b5013430e6 --- /dev/null +++ b/build/pkgs/googletest/distros/alpine.txt @@ -0,0 +1 @@ +gtest-dev diff --git a/build/pkgs/googletest/distros/arch.txt b/build/pkgs/googletest/distros/arch.txt new file mode 100644 index 00000000000..5e17e6e11c8 --- /dev/null +++ b/build/pkgs/googletest/distros/arch.txt @@ -0,0 +1 @@ +gtest diff --git a/build/pkgs/googletest/distros/debian.txt b/build/pkgs/googletest/distros/debian.txt new file mode 100644 index 00000000000..8e2118051ff --- /dev/null +++ b/build/pkgs/googletest/distros/debian.txt @@ -0,0 +1 @@ +googletest diff --git a/build/pkgs/googletest/distros/fedora.txt b/build/pkgs/googletest/distros/fedora.txt new file mode 100644 index 00000000000..5e17e6e11c8 --- /dev/null +++ b/build/pkgs/googletest/distros/fedora.txt @@ -0,0 +1 @@ +gtest diff --git a/build/pkgs/googletest/distros/freebsd.txt b/build/pkgs/googletest/distros/freebsd.txt new file mode 100644 index 00000000000..b5572b17648 --- /dev/null +++ b/build/pkgs/googletest/distros/freebsd.txt @@ -0,0 +1 @@ +devel/googletest diff --git a/build/pkgs/googletest/distros/gentoo.txt b/build/pkgs/googletest/distros/gentoo.txt new file mode 100644 index 00000000000..160c470c7aa --- /dev/null +++ b/build/pkgs/googletest/distros/gentoo.txt @@ -0,0 +1 @@ +dev-cpp/gtest diff --git a/build/pkgs/googletest/distros/homebrew.txt b/build/pkgs/googletest/distros/homebrew.txt new file mode 100644 index 00000000000..8e2118051ff --- /dev/null +++ b/build/pkgs/googletest/distros/homebrew.txt @@ -0,0 +1 @@ +googletest diff --git a/build/pkgs/googletest/distros/nix.txt b/build/pkgs/googletest/distros/nix.txt new file mode 100644 index 00000000000..5e17e6e11c8 --- /dev/null +++ b/build/pkgs/googletest/distros/nix.txt @@ -0,0 +1 @@ +gtest diff --git a/build/pkgs/googletest/distros/opensuse.txt b/build/pkgs/googletest/distros/opensuse.txt new file mode 100644 index 00000000000..8e2118051ff --- /dev/null +++ b/build/pkgs/googletest/distros/opensuse.txt @@ -0,0 +1 @@ +googletest diff --git a/build/pkgs/googletest/distros/repology.txt b/build/pkgs/googletest/distros/repology.txt new file mode 100644 index 00000000000..5e17e6e11c8 --- /dev/null +++ b/build/pkgs/googletest/distros/repology.txt @@ -0,0 +1 @@ +gtest diff --git a/build/pkgs/googletest/distros/void.txt b/build/pkgs/googletest/distros/void.txt new file mode 100644 index 00000000000..721753a34cd --- /dev/null +++ b/build/pkgs/googletest/distros/void.txt @@ -0,0 +1 @@ +gtest-devel diff --git a/build/pkgs/googletest/package-version.txt b/build/pkgs/googletest/package-version.txt new file mode 100644 index 00000000000..42cf0675c56 --- /dev/null +++ b/build/pkgs/googletest/package-version.txt @@ -0,0 +1 @@ +1.15.2 diff --git a/build/pkgs/googletest/spkg-configure.m4 b/build/pkgs/googletest/spkg-configure.m4 new file mode 100644 index 00000000000..40914cddcee --- /dev/null +++ b/build/pkgs/googletest/spkg-configure.m4 @@ -0,0 +1,16 @@ +SAGE_SPKG_CONFIGURE([googletest], [dnl + AC_MSG_CHECKING([whether googletest is available]) + rm -rf conftest_srcdir + mkdir conftest_srcdir + cat > conftest_srcdir/CMakeLists.txt <& ]AS_MESSAGE_LOG_FD[ 2>&1], [dnl + AC_MSG_RESULT([yes]) + ], [dnl + AC_MSG_RESULT([no]) + AS_VAR_SET([sage_spkg_install_googletest], [yes]) + ]) +]) diff --git a/build/pkgs/googletest/spkg-install.in b/build/pkgs/googletest/spkg-install.in new file mode 100644 index 00000000000..8573ba26d7b --- /dev/null +++ b/build/pkgs/googletest/spkg-install.in @@ -0,0 +1,9 @@ +# -*- shell-script -*- +cd src +# Need C++14, so remove our flags that force C++11 +export CXX="$(echo "$CXX" | sed 's/-std=[a-z0-9+]*//g') -std=gnu++14" +mkdir build +cd build +sdh_cmake -DBUILD_SHARED_LIBS=ON .. +sdh_make +sdh_make_install diff --git a/build/pkgs/googletest/type b/build/pkgs/googletest/type new file mode 100644 index 00000000000..134d9bc32d5 --- /dev/null +++ b/build/pkgs/googletest/type @@ -0,0 +1 @@ +optional diff --git a/build/pkgs/sagemath_cmr/SPKG.rst b/build/pkgs/sagemath_cmr/SPKG.rst new file mode 120000 index 00000000000..b4545b4bda6 --- /dev/null +++ b/build/pkgs/sagemath_cmr/SPKG.rst @@ -0,0 +1 @@ +src/README.rst \ No newline at end of file diff --git a/build/pkgs/sagemath_cmr/bootstrap b/build/pkgs/sagemath_cmr/bootstrap new file mode 120000 index 00000000000..40542346a4e --- /dev/null +++ b/build/pkgs/sagemath_cmr/bootstrap @@ -0,0 +1 @@ +../sagelib/bootstrap \ No newline at end of file diff --git a/build/pkgs/sagemath_cmr/dependencies b/build/pkgs/sagemath_cmr/dependencies new file mode 100644 index 00000000000..ddc61e2969d --- /dev/null +++ b/build/pkgs/sagemath_cmr/dependencies @@ -0,0 +1 @@ +$(PYTHON) cmr cysignals $(SAGE_ROOT)/pkgs/sagemath-cmr/sage/libs/cmr/*.pxd $(SAGE_ROOT)/pkgs/sagemath-cmr/sage/matrix/matrix_cmr*.pyx $(SAGE_ROOT)/pkgs/sagemath-cmr/sage/matrix/seymour*.pyx | $(PYTHON_TOOLCHAIN) sage_setup sagemath_environment cython diff --git a/build/pkgs/sagemath_cmr/package-version.txt b/build/pkgs/sagemath_cmr/package-version.txt new file mode 120000 index 00000000000..c4540217bba --- /dev/null +++ b/build/pkgs/sagemath_cmr/package-version.txt @@ -0,0 +1 @@ +src/VERSION.txt \ No newline at end of file diff --git a/build/pkgs/sagemath_cmr/spkg-install.in b/build/pkgs/sagemath_cmr/spkg-install.in new file mode 120000 index 00000000000..e38ca3ace8b --- /dev/null +++ b/build/pkgs/sagemath_cmr/spkg-install.in @@ -0,0 +1 @@ +../sagemath_bliss/spkg-install.in \ No newline at end of file diff --git a/build/pkgs/sagemath_cmr/spkg-src b/build/pkgs/sagemath_cmr/spkg-src new file mode 100755 index 00000000000..0df5a1db65d --- /dev/null +++ b/build/pkgs/sagemath_cmr/spkg-src @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# +# Script to prepare an sdist tarball for sagemath-cmr +# This script is not used during build. +# +# HOW TO MAKE THE TARBALL: +# ./sage --sh build/pkgs/sagemath_cmr/spkg-src + +if [ -z "$SAGE_ROOT" ] ; then + echo >&2 "Error - SAGE_ROOT undefined ... exiting" + echo >&2 "Maybe run 'sage -sh'?" + exit 1 +fi + +# Exit on failure +set -e + +cd build/pkgs/sagemath_cmr + +cd src +# Get rid of old *.egg-info/SOURCES.txt +rm -Rf *.egg-info + +python3 -m build --sdist --no-isolation --skip-dependency-check --outdir "$SAGE_DISTFILES" diff --git a/build/pkgs/sagemath_cmr/src b/build/pkgs/sagemath_cmr/src new file mode 120000 index 00000000000..d2d6820f5ec --- /dev/null +++ b/build/pkgs/sagemath_cmr/src @@ -0,0 +1 @@ +../../../pkgs/sagemath-cmr \ No newline at end of file diff --git a/build/pkgs/sagemath_cmr/type b/build/pkgs/sagemath_cmr/type new file mode 100644 index 00000000000..134d9bc32d5 --- /dev/null +++ b/build/pkgs/sagemath_cmr/type @@ -0,0 +1 @@ +optional diff --git a/build/pkgs/sagemath_cmr/version_requirements.txt b/build/pkgs/sagemath_cmr/version_requirements.txt new file mode 100644 index 00000000000..0b61641bebd --- /dev/null +++ b/build/pkgs/sagemath_cmr/version_requirements.txt @@ -0,0 +1,2 @@ +# This file is updated on every release by the sage-update-version script +sagemath-cmr ~= 10.1 diff --git a/build/pkgs/sagemath_doc_html/dependencies_optional b/build/pkgs/sagemath_doc_html/dependencies_optional index e9b59e89755..91bb2e99598 100644 --- a/build/pkgs/sagemath_doc_html/dependencies_optional +++ b/build/pkgs/sagemath_doc_html/dependencies_optional @@ -1 +1 @@ -jupyter_sphinx +jupyter_sphinx sagemath_cmr diff --git a/pkgs/sagemath-cmr/MANIFEST.in b/pkgs/sagemath-cmr/MANIFEST.in new file mode 100644 index 00000000000..cc9a45b309f --- /dev/null +++ b/pkgs/sagemath-cmr/MANIFEST.in @@ -0,0 +1,21 @@ +include VERSION.txt + +graft sage/libs/cmr +include sage/matrix/*_cmr_*.p* +include sage/matrix/seymour_*.p* + +global-exclude *.c +global-exclude *.cpp + +global-exclude __pycache__ +global-exclude *.py[co] +global-exclude *.bak +global-exclude *.so +global-exclude *~ + +global-exclude all__*.py +global-include all__sagemath_cmr.py + +prune .tox +prune build +prune dist diff --git a/pkgs/sagemath-cmr/README.rst b/pkgs/sagemath-cmr/README.rst new file mode 100644 index 00000000000..46d2baaad90 --- /dev/null +++ b/pkgs/sagemath-cmr/README.rst @@ -0,0 +1,37 @@ +========================================================================== + Sage: Open Source Mathematics Software: Combinatorial matrix recognition +========================================================================== + +About SageMath +-------------- + + "Creating a Viable Open Source Alternative to + Magma, Maple, Mathematica, and MATLAB" + + Copyright (C) 2005-2024 The Sage Development Team + + https://www.sagemath.org + +SageMath fully supports all major Linux distributions, recent versions of +macOS, and Windows (Windows Subsystem for Linux). + +See https://doc.sagemath.org/html/en/installation/index.html +for general installation instructions. + + +About this pip-installable source distribution +---------------------------------------------- + +This pip-installable source distribution ``sagemath-cmr`` is a small +optional distribution for use with ``sagemath-standard``. + +It provides a Cython interface to the CMR library (https://github.com/discopt/cmr), +providing recognition and decomposition algorithms for: + +- Totally Unimodular Matrices +- Network Matrices +- Complement Totally Unimodular Matrices +- (Strongly) k-Modular and Unimodular Matrices +- Regular Matroids +- Graphic / Cographic / Planar Matrices +- Series-Parallel Matroids diff --git a/pkgs/sagemath-cmr/VERSION.txt b/pkgs/sagemath-cmr/VERSION.txt new file mode 100644 index 00000000000..2f52450b31d --- /dev/null +++ b/pkgs/sagemath-cmr/VERSION.txt @@ -0,0 +1 @@ +10.0 diff --git a/pkgs/sagemath-cmr/pyproject.toml.m4 b/pkgs/sagemath-cmr/pyproject.toml.m4 new file mode 100644 index 00000000000..6bfd3be015d --- /dev/null +++ b/pkgs/sagemath-cmr/pyproject.toml.m4 @@ -0,0 +1,36 @@ +include(`sage_spkg_versions_toml.m4')dnl' -*- conf-toml -*- +[build-system] +# Minimum requirements for the build system to execute. +requires = [ + SPKG_INSTALL_REQUIRES_setuptools + SPKG_INSTALL_REQUIRES_sage_conf + SPKG_INSTALL_REQUIRES_sage_setup + SPKG_INSTALL_REQUIRES_sagemath_environment + SPKG_INSTALL_REQUIRES_cython + SPKG_INSTALL_REQUIRES_cysignals +] +build-backend = "setuptools.build_meta" + +[project] +name = "sagemath-cmr" +description = "Sage: Open Source Mathematics Software: Combinatorial matrix recognition" +dependencies = [ + SPKG_INSTALL_REQUIRES_cysignals +] +dynamic = ["version"] +include(`pyproject_toml_metadata.m4')dnl' + +[project.optional-dependencies] +test = [ + SPKG_INSTALL_REQUIRES_sagemath_repl +] + +[project.readme] +file = "README.rst" +content-type = "text/x-rst" + +[tool.setuptools] +include-package-data = false + +[tool.setuptools.dynamic] +version = {file = ["VERSION.txt"]} diff --git a/pkgs/sagemath-cmr/requirements.txt.m4 b/pkgs/sagemath-cmr/requirements.txt.m4 new file mode 100644 index 00000000000..8b6ca03b0b9 --- /dev/null +++ b/pkgs/sagemath-cmr/requirements.txt.m4 @@ -0,0 +1,2 @@ +Cython==esyscmd(`printf $(sed "s/[.]p.*//;" ../cython/package-version.txt)') +sagemath-standard==esyscmd(`printf $(sed "s/[.]p.*//;" ../sagelib/package-version.txt)') diff --git a/pkgs/sagemath-cmr/sage b/pkgs/sagemath-cmr/sage new file mode 120000 index 00000000000..e0da5daa6f2 --- /dev/null +++ b/pkgs/sagemath-cmr/sage @@ -0,0 +1 @@ +../../src/sage \ No newline at end of file diff --git a/pkgs/sagemath-cmr/setup.py b/pkgs/sagemath-cmr/setup.py new file mode 100644 index 00000000000..8a642648986 --- /dev/null +++ b/pkgs/sagemath-cmr/setup.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python + +from distutils import log +from setuptools import setup + +# Work around a Cython problem in Python 3.8.x on macOS +# https://github.com/cython/cython/issues/3262 +import os +if os.uname().sysname == 'Darwin': + import multiprocessing + multiprocessing.set_start_method('fork', force=True) + +# If build isolation is not in use and setuptools_scm is installed, +# then its file_finders entry point is invoked, which we don't need. +# Workaround from ​https://github.com/pypa/setuptools_scm/issues/190#issuecomment-351181286 +try: + import setuptools_scm.integration + setuptools_scm.integration.find_files = lambda _: [] +except ImportError: + pass + +# PEP 517 builds do not have . in sys.path +import sys +sys.path.insert(0, os.path.dirname(__file__)) + +if len(sys.argv) > 1 and (sys.argv[1] == "sdist" or sys.argv[1] == "egg_info"): + sdist = True +else: + sdist = False + +if sdist: + cmdclass = {} +else: + from sage_setup.excepthook import excepthook + sys.excepthook = excepthook + + from sage_setup.setenv import setenv + setenv() + + import sage.env + sage.env.default_required_modules = sage.env.default_optional_modules = () + + from sage_setup.command.sage_build_cython import sage_build_cython + from sage_setup.command.sage_build_ext import sage_build_ext + sage_build_cython.built_distributions = ['sagemath-cmr'] + + cmdclass = dict(build_cython=sage_build_cython, + build_ext=sage_build_ext) + +from sage_setup.find import find_python_sources +python_packages, python_modules, cython_modules = find_python_sources( + '.', ['sage'], distributions=['sagemath-cmr']) + +log.warn('python_packages = {0}'.format(python_packages)) +log.warn('python_modules = {0}'.format(python_modules)) +log.warn('cython_modules = {0}'.format(cython_modules)) + +setup( + cmdclass = cmdclass, + packages = python_packages, + py_modules = python_modules, + ext_modules = cython_modules, +) diff --git a/src/doc/en/developer/sage_manuals.rst b/src/doc/en/developer/sage_manuals.rst index 13f6a3da94a..74f60c23e86 100644 --- a/src/doc/en/developer/sage_manuals.rst +++ b/src/doc/en/developer/sage_manuals.rst @@ -204,6 +204,10 @@ by Sage, you can link toward it without specifying its full path: - ``:mathscinet:`MR0100971``` - :mathscinet:`MR0100971` + * - :ref:`CMR ` + - ``:cmr:`GraphNode ``` + - :cmr:`GraphNode ` + * - :ref:`ECL ` - ``:ecl:`Manipulating-Lisp-objects``` - :ecl:`Manipulating-Lisp-objects` diff --git a/src/doc/en/reference/matrices/index.rst b/src/doc/en/reference/matrices/index.rst index 889a33bd717..9de4c4c6bc6 100644 --- a/src/doc/en/reference/matrices/index.rst +++ b/src/doc/en/reference/matrices/index.rst @@ -101,4 +101,12 @@ objects like operation tables (e.g. the multiplication table of a group). sage/matrix/benchmark +.. ONLY:: feature_sage_libs_cmr + + .. toctree:: + :maxdepth: 1 + + sage/matrix/seymour_decomposition + sage/matrix/matrix_cmr_sparse + .. include:: ../footer.txt diff --git a/src/doc/en/reference/references/index.rst b/src/doc/en/reference/references/index.rst index 6bc9946a442..a0d4b980075 100644 --- a/src/doc/en/reference/references/index.rst +++ b/src/doc/en/reference/references/index.rst @@ -1390,6 +1390,11 @@ REFERENCES: vol. 308, no. 1, July 1988. http://www.ams.org/journals/tran/1988-308-01/S0002-9947-1988-0946427-X/S0002-9947-1988-0946427-X.pdf +.. [BW1988b] Robert E. Bixby, Donald K. Wagner. + *An Almost Linear-Time Algorithm for Graph Realization*. + Mathematics of Operations Research, 1988. + :doi:`10.1287/moor.13.1.99` + .. [BW1993] Thomas Becker and Volker Weispfenning. *Groebner Bases - A Computational Approach To Commutative Algebra*. Springer, New York, 1993. @@ -5829,6 +5834,9 @@ REFERENCES: subsequences*, Canadian Journal of Mathematics, Vol 13 (1961), pp. 179--191. +.. [Sch1986] Alexander Schrijver, *Theory of Linear and Integer Programming*, + John Wiley and Sons, 1986. + .. [Sch1990] Schnyder, Walter. *Embedding Planar Graphs on the Grid*. Proc. 1st Annual ACM-SIAM Symposium on Discrete Algorithms, San Francisco (1994), pp. 138-147. @@ -5907,7 +5915,10 @@ REFERENCES: spaces* in Scientific contributions in honor of Mirian Andrés Gómez, pp 507-519, Univ. La Rioja Serv. Publ., Logroño (2010). -.. [Sey1981] \P. D. Seymour, Nowhere-zero 6-flows, J. Comb. Theory Ser B, +.. [Sey1980] \P. D. Seymour, *Decomposition of regular matroids*, + J. Comb. Theory Ser B, 28 (1980), 305-359. + +.. [Sey1981] \P. D. Seymour, *Nowhere-zero 6-flows*, J. Comb. Theory Ser B, 30 (1981), 130-135. :doi:`10.1016/0095-8956(81)90058-7` .. [SH1995] \C. P. Schnorr and H. H. Hörner. *Attacking the diff --git a/src/sage/all__sagemath_cmr.py b/src/sage/all__sagemath_cmr.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/sage/categories/finite_dimensional_modules_with_basis.py b/src/sage/categories/finite_dimensional_modules_with_basis.py index ee5b6490346..262c4886d5d 100644 --- a/src/sage/categories/finite_dimensional_modules_with_basis.py +++ b/src/sage/categories/finite_dimensional_modules_with_basis.py @@ -10,6 +10,7 @@ # http://www.gnu.org/licenses/ # ***************************************************************************** +import functools import operator from sage.categories.category_with_axiom import CategoryWithAxiom, CategoryWithAxiom_over_base_ring from sage.categories.fields import Fields @@ -888,6 +889,343 @@ def image(self): return C.submodule(self.image_basis(), already_echelonized=True, category=self.category_for()) + @cached_method + def _matrix_cmr(self): + from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + from sage.matrix.matrix_space import MatrixSpace + M = self.matrix() + MS = MatrixSpace(self.base_ring(), M.nrows(), M.ncols(), sparse=True) + return Matrix_cmr_chr_sparse(MS, M) + + @lazy_attribute + def is_unimodular(self): + r""" + Return whether ``self`` is a unimodular morphism. + + This does not depend on the choice of bases for + domain and codomain. + + EXAMPLES:: + + sage: M = matrix(ZZ, [[1, 0, 0], [0, 1, 0]], + ....: column_keys=['a', 'b', 'c'], + ....: row_keys=['v', 'w']); M + Generic morphism: + From: Free module generated by {'a', 'b', 'c'} over Integer Ring + To: Free module generated by {'v', 'w'} over Integer Ring + sage: M.is_unimodular() # needs sage.libs.cmr + True + """ + try: + matrix = self._matrix_cmr() + except (ImportError, TypeError): + matrix = self.matrix() + try: + return matrix.is_unimodular + except AttributeError: + return NotImplemented + + @lazy_attribute + def is_strongly_unimodular(self): + r""" + Return whether ``self`` is a strongly unimodular morphism. + + This does not depend on the choice of bases for + domain and codomain. + + EXAMPLES:: + + sage: M = matrix(ZZ, [[1, 0, 1], [0, 1, 1], [1, 2, 3]], + ....: column_keys=['a', 'b', 'c'], + ....: row_keys=['u', 'v', 'w']); M + Generic morphism: + From: Free module generated by {'a', 'b', 'c'} over Integer Ring + To: Free module generated by {'u', 'v', 'w'} over Integer Ring + sage: M.is_unimodular() # needs sage.libs.cmr + True + sage: M.is_strongly_unimodular() # needs sage.libs.cmr + False + """ + try: + matrix = self._matrix_cmr() + except (ImportError, TypeError): + matrix = self.matrix() + try: + return matrix.is_strongly_unimodular + except AttributeError: + return NotImplemented + + @lazy_attribute + def equimodulus(self): + r""" + Return the integer `k` such that ``self`` is equimodular with determinant gcd `k`. + """ + try: + matrix = self._matrix_cmr() + except (ImportError, TypeError): + matrix = self.matrix() + try: + return matrix.equimodulus + except AttributeError: + return NotImplemented + + @lazy_attribute + def strong_equimodulus(self): + r""" + Return the integer `k` such that ``self`` is strongly equimodular with determinant gcd `k`. + """ + try: + matrix = self._matrix_cmr() + except (ImportError, TypeError): + matrix = self.matrix() + try: + return matrix.strong_equimodulus + except AttributeError: + return NotImplemented + + @lazy_attribute + def is_k_equimodular(self): + r""" + Return whether ``self`` is an equimodular morphism with determinant gcd `k`. + + This does not depend on the choice of bases for + domain and codomain. + + EXAMPLES:: + + sage: M = matrix(ZZ, [[1, 0, 1], [0, 1, 1], [1, 2, 3]], + ....: column_keys=['a', 'b', 'c'], + ....: row_keys=['u', 'v', 'w']); M + Generic morphism: + From: Free module generated by {'a', 'b', 'c'} over Integer Ring + To: Free module generated by {'u', 'v', 'w'} over Integer Ring + sage: M.is_k_equimodular(1) # needs sage.libs.cmr + True + sage: M.is_k_equimodular(2) + False + """ + try: + matrix = self._matrix_cmr() + except (ImportError, TypeError): + matrix = self.matrix() + try: + return matrix.is_k_equimodular + except AttributeError: + return NotImplemented + + @lazy_attribute + def is_strongly_k_equimodular(self): + r""" + Return whether ``self`` is a strongly equimodular morphism with determinant gcd `k`. + + This does not depend on the choice of bases for + domain and codomain. + + EXAMPLES:: + + sage: M = matrix(ZZ, [[1, 0, 1], [0, 1, 1], [1, 2, 3]], + ....: column_keys=['a', 'b', 'c'], + ....: row_keys=['u', 'v', 'w']); M + Generic morphism: + From: Free module generated by {'a', 'b', 'c'} over Integer Ring + To: Free module generated by {'u', 'v', 'w'} over Integer Ring + sage: M.is_strongly_k_equimodular(1) # needs sage.libs.cmr + False + sage: M = matrix(ZZ, [[1, 0, 0], [0, 1, 0]], + ....: column_keys=['a', 'b', 'c'], + ....: row_keys=['v', 'w']); M + Generic morphism: + From: Free module generated by {'a', 'b', 'c'} over Integer Ring + To: Free module generated by {'v', 'w'} over Integer Ring + sage: M.is_strongly_k_equimodular(1) # needs sage.libs.cmr + True + """ + try: + matrix = self._matrix_cmr() + except (ImportError, TypeError): + matrix = self.matrix() + try: + return matrix.is_strongly_k_equimodular + except AttributeError: + return NotImplemented + + def _wrapped_method_with_certificate(self, matrix_method): + + @functools.wraps(matrix_method) + def wrapper(*, certificate=False, **kwds): + if not certificate: + return matrix_method(**kwds) + column_keys = self.domain().basis().keys() + row_keys = self.codomain().basis().keys() + return matrix_method(certificate=True, + column_keys=column_keys, + row_keys=row_keys, + **kwds) + return wrapper + + @lazy_attribute + def is_graphic(self): + r""" + + EXAMPLES:: + + sage: # needs sage.libs.cmr + sage: M = matrix(ZZ, [[1, 0], [-1, 1], [0, -1]], + ....: column_keys=['a', 'b'], + ....: row_keys=['u', 'v', 'w']); M + Generic morphism: + From: Free module generated by {'a', 'b'} over Integer Ring + To: Free module generated by {'u', 'v', 'w'} over Integer Ring + sage: M.is_graphic() + True + sage: result, certificate = M.is_graphic(certificate=True) + sage: graph, forest_edges, coforest_edges = certificate + sage: graph.vertices(sort=True) # TODO: We should see keys + [1, 2, 7, 12] + sage: graph.edges(sort=True, labels=False) + [(1, 2), (1, 7), (1, 12), (2, 7), (7, 12)] + sage: forest_edges # indexed by rows of M + {'u': (1, 2), 'v': (7, 1), 'w': (12, 7)} + sage: coforest_edges # indexed by cols of M + {'a': (2, 7), 'b': (1, 12)} + """ + try: + matrix = self._matrix_cmr() + except (ImportError, TypeError): + matrix = self.matrix() + try: + return self._wrapped_method_with_certificate(matrix.is_graphic) + except AttributeError: + return NotImplemented + + @lazy_attribute + def is_cographic(self): + r""" + + EXAMPLES:: + + sage: # needs sage.libs.cmr + sage: M = matrix([[1, 0, 0, 0, 1, -1, 1, 0, 0], + ....: [0, 1, 0, 0, 0, 1, -1, 1, 0], + ....: [0, 0, 1, 0, 0, 0, 1, -1, 1], + ....: [0, 0, 0, 1, 1, 0, 0, 1, -1]], + ....: column_keys=['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'], + ....: row_keys=range(4)); M + Generic morphism: + From: Free module generated by {'a', 'b', 'c', 'd', + 'e', 'f', 'g', 'h', 'i'} over Integer Ring + To: Free module generated by {0, 1, 2, 3} over Integer Ring + sage: M.is_cographic() + True + """ + try: + matrix = self._matrix_cmr() + except (ImportError, TypeError): + matrix = self.matrix() + try: + return self._wrapped_method_with_certificate(matrix.is_cographic) + except AttributeError: + return NotImplemented + + @lazy_attribute + def is_network(self): + r""" + + EXAMPLES:: + + sage: # needs sage.libs.cmr + sage: M = matrix([[-1, 0, 0, 0, 1, -1, 0], + ....: [ 1, 0, 0, 1, -1, 1, 0], + ....: [ 0, -1, 0, -1, 1, -1, 0], + ....: [ 0, 1, 0, 0, 0, 0, 1], + ....: [ 0, 0, 1, -1, 1, 0, 1], + ....: [ 0, 0, -1, 1, -1, 0, 0]], + ....: column_keys=['a', 'b', 'c', 'd', 'e', 'f', 'g'], + ....: row_keys=range(6)); M + Generic morphism: + From: Free module generated by {'a', 'b', 'c', 'd', 'e', 'f', 'g'} over Integer Ring + To: Free module generated by {0, 1, 2, 3, 4, 5} over Integer Ring + sage: M.is_network() + True + sage: result, certificate = M.is_network(certificate=True) + sage: result, certificate + (True, + (Digraph on 7 vertices, + {0: (9, 8), 1: (3, 8), 2: (3, 4), 3: (5, 4), 4: (4, 6), 5: (0, 6)}, + {'a': (3, 9), 'b': (5, 3), 'c': (4, 0), 'd': (0, 8), + 'e': (9, 0), 'f': (4, 9), 'g': (5, 6)})) + sage: digraph, forest_arcs, coforest_arcs = certificate + sage: digraph.plot(edge_colors={'red': forest_arcs.values()}) # needs sage.plot + Graphics object consisting of 21 graphics primitives + """ + try: + matrix = self._matrix_cmr() + except (ImportError, TypeError): + matrix = self.matrix() + try: + return self._wrapped_method_with_certificate(matrix.is_network_matrix) + except AttributeError: + return NotImplemented + + @lazy_attribute + def is_totally_unimodular(self): + r""" + Return whether the matrix of ``self`` is totally unimodular. + + This depends on the choice of bases for domain and codomain. + + EXAMPLES:: + + sage: # needs sage.libs.cmr + sage: M = matrix(ZZ, [[1, 0], [-1, 1], [0, 1]], + ....: column_keys=['a', 'b'], + ....: row_keys=['u', 'v', 'w']); M + Generic morphism: + From: Free module generated by {'a', 'b'} over Integer Ring + To: Free module generated by {'u', 'v', 'w'} over Integer Ring + sage: M.is_totally_unimodular() + True + sage: result, certificate = M.is_totally_unimodular(certificate=True) + sage: result, certificate + (True, GraphicNode (3×2)) + sage: certificate.graph() + Graph on 4 vertices + """ + try: + matrix = self._matrix_cmr() + except (ImportError, TypeError): + matrix = self.matrix() + try: + return self._wrapped_method_with_certificate(matrix.is_totally_unimodular) + except AttributeError: + return NotImplemented + + @lazy_attribute + def is_complement_totally_unimodular(self): + r""" + """ + try: + matrix = self._matrix_cmr() + except (ImportError, TypeError): + matrix = self.matrix() + try: + return self._wrapped_method_with_certificate(matrix.is_complement_totally_unimodular) + except AttributeError: + return NotImplemented + + @functools.wraps(matrix.is_complement_totally_unimodular) + def is_complement_totally_unimodular(*, certificate=False, **kwds): + if not certificate: + return matrix.is_complement_totally_unimodular(**kwds) + column_keys = self.domain().basis().keys() + row_keys = self.codomain().basis().keys() + return matrix.is_complement_totally_unimodular(certificate=True, + column_keys=column_keys, + row_keys=row_keys, + **kwds) + + return is_complement_totally_unimodular + class Homsets(HomsetsCategory): class Endset(CategoryWithAxiom): diff --git a/src/sage/features/sagemath.py b/src/sage/features/sagemath.py index f536665d8cc..40bc49bc97e 100644 --- a/src/sage/features/sagemath.py +++ b/src/sage/features/sagemath.py @@ -359,6 +359,30 @@ def __init__(self): spkg='sagemath_libbraiding', type='standard') +class sage__libs__cmr(JoinFeature): + r""" + A :class:`sage.features.Feature` describing the presence of :mod:`sage.libs.cmr` + and other modules depending on CMR, the Combinatorial Matrix Recognition library. + + EXAMPLES:: + + sage: from sage.features.sagemath import sage__libs__cmr + sage: sage__libs__cmr().is_present() # optional - sage.libs.cmr + FeatureTestResult('sage.libs.cmr', True) + """ + def __init__(self): + r""" + TESTS:: + + sage: from sage.features.sagemath import sage__libs__cmr + sage: isinstance(sage__libs__cmr(), sage__libs__cmr) + True + """ + JoinFeature.__init__(self, 'sage.libs.cmr', + [PythonModule('sage.matrix.matrix_cmr_sparse')], + spkg='sagemath_cmr', type='optional') + + class sage__libs__ecl(PythonModule): r""" A :class:`~sage.features.Feature` describing the presence of :mod:`sage.libs.ecl`. @@ -1178,6 +1202,7 @@ def all_features(): sage__graphs(), sage__groups(), sage__libs__braiding(), + sage__libs__cmr(), sage__libs__ecl(), sage__libs__flint(), sage__libs__gap(), diff --git a/src/sage/libs/all__sagemath_cmr.py b/src/sage/libs/all__sagemath_cmr.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/sage/libs/cmr/__init__.py b/src/sage/libs/cmr/__init__.py new file mode 100644 index 00000000000..4f89e982ee4 --- /dev/null +++ b/src/sage/libs/cmr/__init__.py @@ -0,0 +1 @@ +# sage_setup: distribution = sagemath-cmr diff --git a/src/sage/libs/cmr/cmr.pxd b/src/sage/libs/cmr/cmr.pxd new file mode 100644 index 00000000000..7cbebd9fa9e --- /dev/null +++ b/src/sage/libs/cmr/cmr.pxd @@ -0,0 +1,579 @@ +# sage_setup: distribution = sagemath-cmr +# -*- python -*- +# distutils: libraries = cmr + +# (progn (replace-regexp "/[*]\\(.\\|\n\\)*?[*]/" "" nil (point) (point-max)) (replace-regexp "[;{}]" "" nil (point) (point-max)) (replace-regexp "CMR_EXPORT *" "" nil (point) (point-max)) (replace-regexp "bool" "bint" nil (point) (point-max))) + +from libc.stdint cimport int8_t, uint32_t, int64_t + +cdef extern from "stdbool.h": + + ctypedef int bool + +cdef extern from "cmr/env.h": + const int CMR_OKAY + const int CMR_ERROR_INPUT + const int CMR_ERROR_OUTPUT + const int CMR_ERROR_MEMORY + const int CMR_ERROR_INVALID + const int CMR_ERROR_OVERFLOW + const int CMR_ERROR_TIMEOUT + + ctypedef int CMR_ERROR + + ctypedef struct CMR + + int CMRcreateEnvironment(CMR** pcmr) + int CMRfreeEnvironment(CMR** pcmr) + + char* CMRgetErrorMessage(CMR* cmr) + void CMRclearErrorMessage(CMR* cmr) + + CMR_ERROR _CMRallocBlock(CMR* cmr, void** ptr, size_t size) + CMR_ERROR _CMRfreeBlock(CMR* cmr, void** ptr, size_t size) + CMR_ERROR _CMRallocBlockArray(CMR* cmr, void** ptr, size_t size, size_t length) + CMR_ERROR _CMRreallocBlockArray(CMR* cmr, void** ptr, size_t size, size_t length) + CMR_ERROR _CMRduplicateBlockArray(CMR* cmr, void** ptr, size_t size, size_t length, void* source) + CMR_ERROR _CMRfreeBlockArray(CMR* cmr, void** ptr) + +cdef extern from "cmr/matrix.h": + + ctypedef struct CMR_SUBMAT: + size_t numRows + size_t* rows + size_t numColumns + size_t* columns + + CMR_ERROR CMRsubmatCreate(CMR* cmr, size_t numRows, size_t numColumns, CMR_SUBMAT** psubmatrix) + CMR_ERROR CMRsubmatCreate1x1(CMR* cmr, size_t row, size_t column, CMR_SUBMAT** psubmatrix) + CMR_ERROR CMRsubmatFree(CMR* cmr, CMR_SUBMAT** psubmatrix) + # CMR_ERROR CMRsubmatPrint(CMR* cmr, CMR_SUBMAT* submatrix, size_t numRows, size_t numColumns, FILE* stream) + + ctypedef struct CMR_INTMAT: + size_t numRows + size_t numColumns + size_t numNonzeros + size_t* rowSlice + size_t* entryColumns + int* entryValues + + CMR_ERROR CMRintmatCreate(CMR* cmr, CMR_INTMAT** presult, int numRows, int numColumns, int numNonzeros) + CMR_ERROR CMRintmatSortNonzeros(CMR* cmr, CMR_INTMAT* matrix) + # CMR_ERROR CMRintmatPrintDense(CMR* cmr, CMR_INTMAT* matrix, FILE* stream, char zeroChar, bint header) + CMR_ERROR CMRintmatFindEntry(CMR_INTMAT* matrix, size_t row, size_t column, size_t* pentry) + CMR_ERROR CMRintmatSlice(CMR* cmr, CMR_INTMAT* matrix, CMR_SUBMAT* submatrix, CMR_INTMAT** presult) + CMR_ERROR CMRintmatFree(CMR* cmr, CMR_INTMAT** pmatrix) + + ctypedef struct CMR_CHRMAT: + size_t numRows + size_t numColumns + size_t numNonzeros + size_t* rowSlice + size_t* entryColumns + char* entryValues + + CMR_ERROR CMRchrmatCreate(CMR* cmr, CMR_CHRMAT** presult, int numRows, int numColumns, int numNonzeros) + CMR_ERROR CMRchrmatSortNonzeros(CMR* cmr, CMR_CHRMAT* matrix) + # CMR_ERROR CMRchrmatPrintDense(CMR* cmr, CMR_CHRMAT* matrix, FILE* stream, char zeroChar, bint header) + CMR_ERROR CMRchrmatFindEntry(CMR_CHRMAT* matrix, size_t row, size_t column, size_t* pentry) + CMR_ERROR CMRchrmatSlice(CMR* cmr, CMR_CHRMAT* matrix, CMR_SUBMAT* submatrix, CMR_CHRMAT** presult) + CMR_ERROR CMRchrmatFree(CMR* cmr, CMR_CHRMAT** pmatrix) + + CMR_ERROR CMRchrmatToInt(CMR* cmr, CMR_CHRMAT* matrix, CMR_INTMAT** presult) + CMR_ERROR CMRintmatToChr(CMR* cmr, CMR_INTMAT* matrix, CMR_CHRMAT** presult) + +cdef extern from "cmr/camion.h": + + ctypedef struct CMR_CAMION_STATISTICS: + uint32_t generalCount + double generalTime + uint32_t graphCount + double graphTime + uint32_t totalCount + double totalTime + + CMR_ERROR CMRcamionStatsInit(CMR_CAMION_STATISTICS* stats) + # CMR_ERROR CMRstatsCamionPrint(FILE* stream, CMR_CAMION_STATISTICS* stats, const char* prefix) + CMR_ERROR CMRcamionTestSigns(CMR* cmr, CMR_CHRMAT* matrix, bool* pisCamionSigned, CMR_SUBMAT** psubmatrix, CMR_CAMION_STATISTICS* stats, double timeLimit) + CMR_ERROR CMRcamionComputeSigns(CMR* cmr, CMR_CHRMAT* matrix, bool* pwasCamionSigned, CMR_SUBMAT** psubmatrix, CMR_CAMION_STATISTICS* stats, double timeLimit) + +cdef extern from "cmr/element.h": + + ctypedef int CMR_ELEMENT + + const char* CMRelementString(CMR_ELEMENT element, char* buffer) + bint CMRelementIsValid(CMR_ELEMENT element) + CMR_ELEMENT CMRrowToElement(size_t row) + CMR_ELEMENT CMRcolumnToElement(size_t column) + bint CMRelementIsRow(CMR_ELEMENT element) + size_t CMRelementToRowIndex(CMR_ELEMENT element) + bint CMRelementIsColumn(CMR_ELEMENT element) + size_t CMRelementToColumnIndex(CMR_ELEMENT element) + CMR_ELEMENT CMRelementTranspose(CMR_ELEMENT element) + +cdef extern from "cmr/graph.h": + + ctypedef int CMR_GRAPH_NODE + ctypedef int CMR_GRAPH_EDGE + ctypedef int CMR_GRAPH_ITER + + ctypedef struct CMR_GRAPH_NODE_DATA: + int prev + int next + int firstOut + + ctypedef struct CMR_GRAPH_ARC_DATA: + int target + int prev + int next + + ctypedef struct CMR_GRAPH: + size_t numNodes + size_t memNodes + CMR_GRAPH_NODE_DATA* nodes + int firstNode + int freeNode + size_t numEdges + size_t memEdges + CMR_GRAPH_ARC_DATA* arcs + int freeEdge + + size_t CMRgraphMemNodes(CMR_GRAPH* graph) + size_t CMRgraphNumNodes(CMR_GRAPH* graph) + size_t CMRgraphMemEdges(CMR_GRAPH* graph) + size_t CMRgraphNumEdges(CMR_GRAPH* graph) + CMR_GRAPH_NODE CMRgraphEdgeU(CMR_GRAPH* graph, CMR_GRAPH_EDGE e) + CMR_GRAPH_NODE CMRgraphEdgeV(CMR_GRAPH* graph, CMR_GRAPH_EDGE e) + CMR_ERROR CMRgraphCreateEmpty(CMR* cmr, CMR_GRAPH** pgraph, int memNodes, int memEdges) + CMR_ERROR CMRgraphFree(CMR* cmr, CMR_GRAPH** pgraph) + CMR_ERROR CMRgraphClear(CMR* cmr, CMR_GRAPH* graph) + CMR_ERROR CMRgraphAddNode(CMR* cmr, CMR_GRAPH* graph, CMR_GRAPH_NODE* pnode) + CMR_ERROR CMRgraphAddEdge(CMR* cmr, CMR_GRAPH* graph, CMR_GRAPH_NODE u, CMR_GRAPH_NODE v, CMR_GRAPH_EDGE* pedge) + CMR_ERROR CMRgraphDeleteNode(CMR* cmr, CMR_GRAPH* graph, CMR_GRAPH_NODE v) + CMR_ERROR CMRgraphDeleteEdge(CMR* cmr, CMR_GRAPH* graph, CMR_GRAPH_EDGE e) + CMR_GRAPH_NODE CMRgraphNodesFirst(CMR_GRAPH* graph) + bint CMRgraphNodesValid(CMR_GRAPH* graph, CMR_GRAPH_NODE v) + CMR_GRAPH_NODE CMRgraphNodesNext(CMR_GRAPH* graph, CMR_GRAPH_NODE v) + CMR_GRAPH_ITER CMRgraphIncFirst(CMR_GRAPH* graph, CMR_GRAPH_NODE v) + bint CMRgraphIncValid(CMR_GRAPH* graph, CMR_GRAPH_ITER i) + CMR_GRAPH_ITER CMRgraphIncNext(CMR_GRAPH* graph, CMR_GRAPH_ITER i) + CMR_GRAPH_EDGE CMRgraphIncEdge(CMR_GRAPH* graph, CMR_GRAPH_ITER i) + CMR_GRAPH_NODE CMRgraphIncSource(CMR_GRAPH* graph, CMR_GRAPH_ITER i) + CMR_GRAPH_NODE CMRgraphIncTarget(CMR_GRAPH* graph, CMR_GRAPH_ITER i) + CMR_GRAPH_ITER CMRgraphEdgesFirst(CMR_GRAPH* graph) + CMR_GRAPH_ITER CMRgraphEdgesNext(CMR_GRAPH* graph, CMR_GRAPH_ITER i) + bint CMRgraphEdgesValid(CMR_GRAPH* graph, CMR_GRAPH_ITER i) + CMR_GRAPH_EDGE CMRgraphEdgesEdge(CMR_GRAPH* graph, CMR_GRAPH_ITER i) + # CMR_ERROR CMRgraphPrint(CMR_GRAPH* graph, FILE* stream) + CMR_ERROR CMRgraphMergeNodes(CMR* cmr, CMR_GRAPH* graph, CMR_GRAPH_NODE u, CMR_GRAPH_NODE v) + # CMR_ERROR CMRgraphCreateFromEdgeList(CMR* cmr, CMR_GRAPH** pgraph, CMR_ELEMENT** pedgeElements, char*** pnodeLabels, FILE* stream) + # CMR_ERROR CMRgraphCopy(CMR* cmr, CMR_GRAPH* graph, CMR_GRAPH** pcopy) + +cdef extern from "cmr/matroid.h": + + CMR_ERROR CMRchrmatBinaryPivot(CMR* cmr, CMR_CHRMAT* matrix, size_t pivotRow, size_t pivotColumn, CMR_CHRMAT** presult) + CMR_ERROR CMRchrmatTernaryPivot(CMR* cmr, CMR_CHRMAT* matrix, size_t pivotRow, size_t pivotColumn, CMR_CHRMAT** presult) + CMR_ERROR CMRchrmatBinaryPivots(CMR* cmr, CMR_CHRMAT* matrix, size_t numPivots, size_t* pivotRows, size_t* pivotColumns, CMR_CHRMAT** presult) + CMR_ERROR CMRchrmatTernaryPivots(CMR* cmr, CMR_CHRMAT* matrix, size_t numPivots, size_t* pivotRows, size_t* pivotColumns, CMR_CHRMAT** presult) + + ctypedef int CMR_MINOR_TYPE + + const int CMR_MINOR_TYPE_DETERMINANT + const int CMR_MINOR_TYPE_ENTRY + const int CMR_MINOR_TYPE_CUSTOM + const int CMR_MINOR_TYPE_U24 + const int CMR_MINOR_TYPE_FANO + const int CMR_MINOR_TYPE_FANO_DUAL + const int CMR_MINOR_TYPE_K5 + const int CMR_MINOR_TYPE_K5_DUAL + const int CMR_MINOR_TYPE_K33 + const int CMR_MINOR_TYPE_K33_DUAL + + ctypedef struct CMR_MINOR: + size_t numPivots + size_t* pivotRows + size_t* pivotColumns + CMR_SUBMAT* remainingSubmatrix + CMR_MINOR_TYPE type + + CMR_ERROR CMRminorCreate(CMR* cmr, CMR_MINOR** pminor, size_t numPivots, CMR_SUBMAT* submatrix, CMR_MINOR_TYPE type) + CMR_ERROR CMRminorFree(CMR* cmr, CMR_MINOR** pminor) + CMR_MINOR_TYPE CMRminorType(CMR_MINOR* minor) + size_t CMRminorNumPivots(CMR_MINOR* minor) + size_t* CMRminorPivotRows(CMR_MINOR* minor) + size_t* CMRminorPivotColumns(CMR_MINOR* minor) + CMR_SUBMAT* CMRminorSubmatrix(CMR_MINOR* minor) + # CMR_ERROR CMRminorWriteToFile(CMR* cmr, CMR_MINOR* minor, size_t numRows, size_t numColumns, const char* fileName) + +cdef extern from "cmr/separation.h": + + ctypedef int CMR_SEPA_FLAGS + + const int CMR_SEPA_FIRST + const int CMR_SEPA_SECOND + const int CMR_SEPA_FLAG_RANK1 + const int CMR_SEPA_FLAG_RANK2 + const int CMR_SEPA_MASK_CHILD + const int CMR_SEPA_MASK_EXTRA + + ctypedef int CMR_SEPA_TYPE + + const int CMR_SEPA_TYPE_TWO + const int CMR_SEPA_TYPE_THREE_DISTRIBUTED_RANKS + const int CMR_SEPA_TYPE_THREE_CONCENTRATED_RANK + + ctypedef struct CMR_SEPA: + size_t numRows + size_t numColumns + CMR_SEPA_FLAGS* rowsFlags + CMR_SEPA_FLAGS* columnsFlags + CMR_SEPA_TYPE type + + CMR_ERROR CMRsepaCreate(CMR* cmr, size_t numRows, size_t numColumns, CMR_SEPA** psepa) + CMR_ERROR CMRsepaFree(CMR* cmr, CMR_SEPA** psepa) + CMR_ERROR CMRsepaComputeSizes(CMR_SEPA* sepa, size_t* pnumRowsTopLeft, size_t* pnumColumnsTopLeft, size_t* pnumRowsBottomRight, size_t* pnumColumnsBottomRight) + CMR_ERROR CMRsepaFindBinaryRepresentatives(CMR* cmr, CMR_SEPA* sepa, CMR_CHRMAT* matrix, CMR_CHRMAT* transpose, bool* pswapped, CMR_SUBMAT** pviolator) + CMR_ERROR CMRsepaFindBinaryRepresentativesSubmatrix(CMR* cmr, CMR_SEPA* sepa, CMR_CHRMAT* matrix, CMR_CHRMAT* transpose, CMR_SUBMAT* submatrix, bool* pswapped, CMR_SUBMAT** pviolator) + CMR_ERROR CMRsepaGetRepresentatives(CMR_SEPA* sepa, size_t reprRows[2][3], size_t reprColumns[2][3]) + CMR_ERROR CMRsepaGetProjection(CMR_SEPA* sepa, size_t part, size_t* rowsToPart, size_t* columnsToPart, size_t* pnumPartRows, size_t* pnumPartColumns) + CMR_ERROR CMRsepaCheckTernary(CMR* cmr, CMR_SEPA* sepa, CMR_CHRMAT* matrix, bool* pisTernary, CMR_SUBMAT** pviolator) + CMR_ERROR CMRsepaCheckTernarySubmatrix(CMR* cmr, CMR_SEPA* sepa, CMR_CHRMAT* matrix, CMR_SUBMAT* submatrix, bool* pisTernary, CMR_SUBMAT** pviolator) + CMR_ERROR CMRoneSum(CMR* cmr, CMR_CHRMAT* first, CMR_CHRMAT* second, CMR_CHRMAT** presult) + CMR_ERROR CMRtwoSum(CMR* cmr, CMR_CHRMAT* first, CMR_CHRMAT* second, CMR_ELEMENT firstMarker, CMR_ELEMENT secondMarker, int8_t characteristic, CMR_CHRMAT** presult) + CMR_ERROR CMRthreeSum(CMR* cmr, CMR_CHRMAT* first, CMR_CHRMAT* second, CMR_ELEMENT firstMarker1, CMR_ELEMENT secondMarker1, CMR_ELEMENT firstMarker2, CMR_ELEMENT secondMarker2, int8_t characteristic, CMR_CHRMAT** presult) + +cdef extern from "cmr/graphic.h": + + ctypedef struct CMR_GRAPHIC_STATISTICS: + uint32_t totalCount + double totalTime + uint32_t checkCount + double checkTime + uint32_t applyCount + double applyTime + uint32_t transposeCount + double transposeTime + + CMR_ERROR CMRgraphicStatsInit(CMR_GRAPHIC_STATISTICS* stats) + # CMR_ERROR CMRgraphicStatsPrint(FILE* stream, CMR_GRAPHIC_STATISTICS* stats, const char* prefix) + CMR_ERROR CMRgraphicComputeMatrix(CMR* cmr, CMR_GRAPH* graph, CMR_CHRMAT** pmatrix, CMR_CHRMAT** ptranspose, int numForestEdges, CMR_GRAPH_EDGE* forestEdges, int numCoforestEdges, CMR_GRAPH_EDGE* coforestEdges, bool* pisCorrectForest) + CMR_ERROR CMRgraphicTestMatrix(CMR* cmr, CMR_CHRMAT* matrix, bool* pisGraphic, CMR_GRAPH** pgraph, CMR_GRAPH_EDGE** pforestEdges, CMR_GRAPH_EDGE** pcoforestEdges, CMR_SUBMAT** psubmatrix, CMR_GRAPHIC_STATISTICS* stats, double timeLimit) + CMR_ERROR CMRgraphicTestTranspose(CMR* cmr, CMR_CHRMAT* matrix, bool* pisCographic, CMR_GRAPH** pgraph, CMR_GRAPH_EDGE** pforestEdges, CMR_GRAPH_EDGE** pcoforestEdges, CMR_SUBMAT** psubmatrix, CMR_GRAPHIC_STATISTICS* stats, double timeLimit) + CMR_ERROR CMRgraphicTestColumnSubmatrixGreedy(CMR* cmr, CMR_CHRMAT* transpose, size_t* orderedColumns, CMR_SUBMAT** psubmatrix) + +cdef extern from "cmr/series_parallel.h": + + ctypedef struct CMR_SP_STATISTICS: + uint32_t totalCount + double totalTime + uint32_t reduceCount + double reduceTime + uint32_t wheelCount + double wheelTime + uint32_t nonbinaryCount + double nonbinaryTime + + CMR_ERROR CMRspStatsInit(CMR_SP_STATISTICS* stats) + + # CMR_ERROR CMRspStatsPrint(FILE* stream, CMR_SP_STATISTICS* stats, const char* prefix) + + ctypedef struct CMR_SP_REDUCTION: + CMR_ELEMENT element + CMR_ELEMENT mate + + char* CMRspReductionString(CMR_SP_REDUCTION reduction, char* buffer) + + bint CMRspIsRow(CMR_SP_REDUCTION reduction) + bint CMRspIsColumn(CMR_SP_REDUCTION reduction) + bint CMRspIsZero(CMR_SP_REDUCTION reduction) + bint CMRspIsUnit(CMR_SP_REDUCTION reduction) + bint CMRspIsCopy(CMR_SP_REDUCTION reduction) + bint CMRspIsValid(CMR_SP_REDUCTION reduction) + + CMR_ERROR CMRspTestBinary(CMR* cmr, CMR_CHRMAT* matrix, bool* pisSeriesParallel, CMR_SP_REDUCTION* reductions, size_t* pnumReductions, CMR_SUBMAT** preducedSubmatrix, CMR_SUBMAT** pviolatorSubmatrix, CMR_SP_STATISTICS* stats, double timeLimit) + CMR_ERROR CMRspTestTernary(CMR* cmr, CMR_CHRMAT* matrix, bool* pisSeriesParallel, CMR_SP_REDUCTION* reductions, size_t* pnumReductions, CMR_SUBMAT** preducedSubmatrix, CMR_SUBMAT** pviolatorSubmatrix, CMR_SP_STATISTICS* stats, double timeLimit) + CMR_ERROR CMRspDecomposeBinary(CMR* cmr, CMR_CHRMAT* matrix, bool* pisSeriesParallel, CMR_SP_REDUCTION* reductions, size_t maxNumReductions, size_t* pnumReductions, CMR_SUBMAT** preducedSubmatrix, CMR_SUBMAT** pviolatorSubmatrix, CMR_SEPA** pseparation, CMR_SP_STATISTICS* stats, double timeLimit) + CMR_ERROR CMRspDecomposeTernary(CMR* cmr, CMR_CHRMAT* matrix, bool* pisSeriesParallel, CMR_SP_REDUCTION* reductions, size_t maxNumReductions, size_t* pnumReductions, CMR_SUBMAT** preducedSubmatrix, CMR_SUBMAT** pviolatorSubmatrix, CMR_SEPA** pseparation, CMR_SP_STATISTICS* stats, double timeLimit) + +cdef extern from "cmr/network.h": + + ctypedef struct CMR_NETWORK_STATISTICS: + uint32_t totalCount + double totalTime + CMR_CAMION_STATISTICS camion + CMR_GRAPHIC_STATISTICS graphic + + CMR_ERROR CMRnetworkStatsInit(CMR_NETWORK_STATISTICS* stats) + # CMR_ERROR CMRstatsNetworkPrint(FILE* stream, CMR_NETWORK_STATISTICS* stats, const char* prefix) + CMR_ERROR CMRnetworkComputeMatrix(CMR* cmr, CMR_GRAPH* digraph, CMR_CHRMAT** pmatrix, CMR_CHRMAT** ptranspose, bool* arcsReversed, int numForestArcs, CMR_GRAPH_EDGE* forestArcs, int numCoforestArcs, CMR_GRAPH_EDGE* coforestArcs, bool* pisCorrectForest) + CMR_ERROR CMRnetworkTestMatrix(CMR* cmr, CMR_CHRMAT* matrix, bool* pisNetwork, bool* psupportIsGraphic, CMR_GRAPH** pdigraph, CMR_GRAPH_EDGE** pforestArcs, CMR_GRAPH_EDGE** pcoforestArcs, bool** parcsReversed, CMR_SUBMAT** psubmatrix, CMR_NETWORK_STATISTICS* stats, double timeLimit) + CMR_ERROR CMRnetworkTestTranspose(CMR* cmr, CMR_CHRMAT* matrix, bool* pisConetwork, bool* psupportIsCographic, CMR_GRAPH** pdigraph, CMR_GRAPH_EDGE** pforestArcs, CMR_GRAPH_EDGE** pcoforestArcs, bool** parcsReversed, CMR_SUBMAT** psubmatrix, CMR_NETWORK_STATISTICS* stats, double timeLimit) + + +cdef extern from "cmr/seymour.h": + + ctypedef int CMR_SEYMOUR_THREESUM_FLAG + + const int CMR_SEYMOUR_THREESUM_FLAG_NO_PIVOTS + const int CMR_SEYMOUR_THREESUM_FLAG_DISTRIBUTED_RANKS + const int CMR_SEYMOUR_THREESUM_FLAG_CONCENTRATED_RANK + const int CMR_SEYMOUR_THREESUM_FLAG_FIRST_WIDE + const int CMR_SEYMOUR_THREESUM_FLAG_FIRST_TALL + const int CMR_SEYMOUR_THREESUM_FLAG_FIRST_MIXED + const int CMR_SEYMOUR_THREESUM_FLAG_FIRST_ALLREPR + const int CMR_SEYMOUR_THREESUM_FLAG_SECOND_WIDE + const int CMR_SEYMOUR_THREESUM_FLAG_SECOND_TALL + const int CMR_SEYMOUR_THREESUM_FLAG_SECOND_MIXED + const int CMR_SEYMOUR_THREESUM_FLAG_SECOND_ALLREPR + const int CMR_SEYMOUR_THREESUM_FLAG_SEYMOUR + const int CMR_SEYMOUR_THREESUM_FLAG_TRUEMPER + + ctypedef struct CMR_SEYMOUR_PARAMS: + bool stopWhenIrregular + bool stopWhenNongraphic + bool stopWhenNoncographic + bool stopWhenNeitherGraphicNorCoGraphic + bool seriesParallel + bool planarityCheck + bool directGraphicness + bool preferGraphicness + bool threeSumPivotChildren + int threeSumStrategy + bool constructLeafGraphs + bool constructAllGraphs + + CMR_ERROR CMRseymourParamsInit(CMR_SEYMOUR_PARAMS* params) + + ctypedef struct CMR_SEYMOUR_STATS: + uint32_t totalCount + double totalTime + CMR_SP_STATISTICS seriesParallel + CMR_GRAPHIC_STATISTICS graphic + CMR_NETWORK_STATISTICS network + uint32_t sequenceExtensionCount + double sequenceExtensionTime + uint32_t sequenceGraphicCount + double sequenceGraphicTime + uint32_t enumerationCount + double enumerationTime + uint32_t enumerationCandidatesCount + + CMR_ERROR CMRseymourStatsInit(CMR_SEYMOUR_STATS* stats) + # CMR_ERROR CMRseymourStatsPrint(FILE* stream, CMR_SEYMOUR_STATS* stats, const char* prefix) + + ctypedef struct CMR_SEYMOUR_NODE + + ctypedef int CMR_SEYMOUR_NODE_TYPE + + const int CMR_SEYMOUR_NODE_TYPE_IRREGULAR + const int CMR_SEYMOUR_NODE_TYPE_UNKNOWN + const int CMR_SEYMOUR_NODE_TYPE_ONE_SUM + const int CMR_SEYMOUR_NODE_TYPE_TWO_SUM + const int CMR_SEYMOUR_NODE_TYPE_THREE_SUM + const int CMR_SEYMOUR_NODE_TYPE_SERIES_PARALLEL + const int CMR_SEYMOUR_NODE_TYPE_PIVOTS + const int CMR_SEYMOUR_NODE_TYPE_GRAPH + const int CMR_SEYMOUR_NODE_TYPE_COGRAPH + const int CMR_SEYMOUR_NODE_TYPE_PLANAR + const int CMR_SEYMOUR_NODE_TYPE_R10 + + bool CMRseymourIsTernary(CMR_SEYMOUR_NODE* node) + bool CMRseymourThreeSumDistributedRanks(CMR_SEYMOUR_NODE* node) + bool CMRseymourThreeSumConcentratedRank(CMR_SEYMOUR_NODE* node) + bool CMRseymourHasTranspose(CMR_SEYMOUR_NODE* node) + CMR_CHRMAT* CMRseymourGetMatrix(CMR_SEYMOUR_NODE* node) + CMR_CHRMAT* CMRseymourGetTranspose(CMR_SEYMOUR_NODE* node) + size_t CMRseymourNumChildren(CMR_SEYMOUR_NODE* node) + CMR_SEYMOUR_NODE* CMRseymourChild(CMR_SEYMOUR_NODE* node, size_t childIndex) + CMR_SEYMOUR_NODE_TYPE CMRseymourType(CMR_SEYMOUR_NODE* node) + size_t CMRseymourNumMinors(CMR_SEYMOUR_NODE* node) + CMR_MINOR* CMRseymourMinor(CMR_SEYMOUR_NODE* node, size_t minorIndex) + int8_t CMRseymourGraphicness(CMR_SEYMOUR_NODE* node) + int8_t CMRseymourCographicness(CMR_SEYMOUR_NODE* node) + int8_t CMRseymourRegularity(CMR_SEYMOUR_NODE* node) + size_t CMRseymourNumRows(CMR_SEYMOUR_NODE* node) + size_t CMRseymourNumColumns(CMR_SEYMOUR_NODE* node) + CMR_ELEMENT* CMRseymourChildRowsToParent(CMR_SEYMOUR_NODE* node, size_t childIndex) + CMR_ELEMENT* CMRseymourChildColumnsToParent(CMR_SEYMOUR_NODE* node, size_t childIndex) + CMR_GRAPH* CMRseymourGraph(CMR_SEYMOUR_NODE* node) + CMR_GRAPH_EDGE* CMRseymourGraphForest(CMR_SEYMOUR_NODE* node) + size_t CMRseymourGraphSizeForest(CMR_SEYMOUR_NODE* node) + CMR_GRAPH_EDGE* CMRseymourGraphCoforest(CMR_SEYMOUR_NODE* node) + size_t CMRseymourGraphSizeCoforest(CMR_SEYMOUR_NODE* node) + bool* CMRseymourGraphArcsReversed(CMR_SEYMOUR_NODE* node) + CMR_GRAPH* CMRseymourCograph(CMR_SEYMOUR_NODE* node) + size_t CMRseymourCographSizeForest(CMR_SEYMOUR_NODE* node) + CMR_GRAPH_EDGE* CMRseymourCographForest(CMR_SEYMOUR_NODE* node) + size_t CMRseymourCographSizeCoforest(CMR_SEYMOUR_NODE* node) + CMR_GRAPH_EDGE* CMRseymourCographCoforest(CMR_SEYMOUR_NODE* node) + bool* CMRseymourCographArcsReversed(CMR_SEYMOUR_NODE* node) + size_t CMRseymourNumPivots(CMR_SEYMOUR_NODE* node) + size_t* CMRseymourPivotRows(CMR_SEYMOUR_NODE* node) + size_t* CMRseymourPivotColumns(CMR_SEYMOUR_NODE* node) + size_t CMRseymourGetUsed(CMR_SEYMOUR_NODE* node) + # CMR_ERROR CMRseymourPrint(CMR* cmr, CMR_SEYMOUR_NODE* node, FILE* stream, bool printChildren, bool printParentElements, bool printMatrices, bool printGraphs, bool printReductions, bool printPivots) + CMR_ERROR CMRseymourCapture(CMR* cmr, CMR_SEYMOUR_NODE* node) + CMR_ERROR CMRseymourRelease(CMR* cmr, CMR_SEYMOUR_NODE** pnode) + CMR_ERROR CMRseymourCreate(CMR* cmr, CMR_SEYMOUR_NODE** pnode, bool isTernary, CMR_CHRMAT* matrix) + CMR_ERROR CMRseymourCloneUnknown(CMR* cmr, CMR_SEYMOUR_NODE* node, CMR_SEYMOUR_NODE** pclone) + CMR_ERROR CMRseymourCloneSubtrees(CMR* cmr, size_t numSubtrees, CMR_SEYMOUR_NODE** subtreeRoots,CMR_SEYMOUR_NODE** clonedSubtrees) + + +cdef extern from "cmr/regular.h": + + ctypedef struct CMR_REGULAR_PARAMS: + CMR_SEYMOUR_PARAMS seymour + + CMR_ERROR CMRregularParamsInit(CMR_REGULAR_PARAMS* params) + + ctypedef struct CMR_REGULAR_STATS: + CMR_SEYMOUR_STATS seymour + + CMR_ERROR CMRregularStatsInit(CMR_REGULAR_STATS* stats) + # CMR_ERROR CMRstatsRegularPrint(FILE* stream, CMR_REGULAR_STATS* stats, const char* prefix) + CMR_ERROR CMRregularTest(CMR* cmr, CMR_CHRMAT* matrix, bint *pisRegular, CMR_SEYMOUR_NODE** pnode, CMR_MINOR** pminor, CMR_REGULAR_PARAMS* params, CMR_REGULAR_STATS* stats, double timeLimit) + CMR_ERROR CMRregularCompleteDecomposition(CMR* cmr, CMR_SEYMOUR_NODE* node, CMR_REGULAR_PARAMS* params, CMR_REGULAR_STATS* stats, double timeLimit) + CMR_ERROR CMRregularRefineDecomposition(CMR* cmr, size_t numNodes, CMR_SEYMOUR_NODE** nodes, CMR_REGULAR_PARAMS* params, CMR_REGULAR_STATS* stats, double timeLimit) + + +cdef extern from "cmr/tu.h": + + const int CMR_TU_ALGORITHM_DECOMPOSITION + const int CMR_TU_ALGORITHM_EULERIAN + const int CMR_TU_ALGORITHM_PARTITION + + ctypedef int CMR_TU_ALGORITHM + + ctypedef struct CMR_TU_PARAMS: + CMR_TU_ALGORITHM algorithm + CMR_SEYMOUR_PARAMS seymour + bool ternary + bool camionFirst + bool naiveSubmatrix + + CMR_ERROR CMRtuParamsInit(CMR_TU_PARAMS* params) + + ctypedef struct CMR_TU_STATS: + CMR_SEYMOUR_STATS seymour + CMR_CAMION_STATISTICS camion + + uint32_t enumerationRowSubsets + uint32_t enumerationColumnSubsets + double enumerationTime + + uint32_t partitionRowSubsets + uint32_t partitionColumnSubsets + double partitionTime + + CMR_ERROR CMRtuStatsInit(CMR_TU_STATS* stats) + # CMR_ERROR CMRtuStatsPrint(FILE* stream, CMR_TU_STATS* stats, const char* prefix) + CMR_ERROR CMRtuTest(CMR* cmr, CMR_CHRMAT* matrix, bool* pisTotallyUnimodular, CMR_SEYMOUR_NODE** proot, CMR_SUBMAT** psubmatrix, CMR_TU_PARAMS* params, CMR_TU_STATS* stats, double timeLimit) + CMR_ERROR CMRtuCompleteDecomposition(CMR* cmr, CMR_SEYMOUR_NODE* node, CMR_TU_PARAMS* params, CMR_TU_STATS* stats, double timeLimit) + + +cdef extern from "cmr/equimodular.h": + + ctypedef struct CMR_EQUIMODULAR_PARAMS: + CMR_TU_PARAMS tu + + CMR_ERROR CMRequimodularParamsInit(CMR_EQUIMODULAR_PARAMS* params) + + ctypedef struct CMR_EQUIMODULAR_STATS: + uint32_t totalCount + double totalTime + double linalgTime + CMR_TU_STATS tu + + CMR_ERROR CMRequimodularStatsInit(CMR_EQUIMODULAR_STATS* stats) + + CMR_ERROR CMRequimodularTest(CMR* cmr, CMR_INTMAT* matrix, + bool* pisEquimodular, int64_t *pgcdDet, + CMR_EQUIMODULAR_PARAMS* params, CMR_EQUIMODULAR_STATS* stats, + double timeLimit) + CMR_ERROR CMRequimodularTestStrong(CMR* cmr, CMR_INTMAT* matrix, + bool* pisStronglyEquimodular, int64_t *pgcdDet, + CMR_EQUIMODULAR_PARAMS* params, CMR_EQUIMODULAR_STATS* stats, + double timeLimit) + CMR_ERROR CMRunimodularTest(CMR* cmr, CMR_INTMAT* matrix, + bool* pisUnimodular, + CMR_EQUIMODULAR_PARAMS* params, CMR_EQUIMODULAR_STATS* stats, + double timeLimit) + CMR_ERROR CMRunimodularTestStrong(CMR* cmr, CMR_INTMAT* matrix, + bool* pisStronglyUnimodular, + CMR_EQUIMODULAR_PARAMS* params, CMR_EQUIMODULAR_STATS* stats, + double timeLimit) + + +cdef extern from "cmr/ctu.h": + + ctypedef struct CMR_CTU_PARAMS: + CMR_TU_PARAMS tu + + CMR_ERROR CMRctuParamsInit(CMR_CTU_PARAMS* params) + + ctypedef struct CMR_CTU_STATS: + uint32_t totalCount + double totalTime + CMR_TU_STATS tu + + CMR_ERROR CMRstatsComplementTotalUnimodularityInit(CMR_CTU_STATS* stats) + # CMR_ERROR CMRstatsComplementTotalUnimodularityPrint(FILE* stream, CMR_CTU_STATS* stats, const char* prefix) + CMR_ERROR CMRctuComplementRowColumn(CMR* cmr, CMR_CHRMAT* matrix, size_t complementRow, size_t complementColumn, CMR_CHRMAT** presult) + CMR_ERROR CMRctuTest(CMR* cmr, CMR_CHRMAT* matrix, bool* pisComplementTotallyUnimodular, size_t* pcomplementRow, size_t* pcomplementColumn, CMR_CTU_PARAMS* params, CMR_CTU_STATS* stats, double timeLimit) + + +cdef extern from "cmr/balanced.h": + ctypedef int CMR_BALANCED_ALGORITHM + + const int CMR_BALANCED_ALGORITHM_AUTO + const int CMR_BALANCED_ALGORITHM_SUBMATRIX + const int CMR_BALANCED_ALGORITHM_GRAPH + + ctypedef struct CMR_BALANCED_PARAMS: + CMR_BALANCED_ALGORITHM algorithm + bool seriesParallel + + ctypedef struct CMR_BALANCED_STATS: + uint32_t totalCount + double totalTime + CMR_SP_STATISTICS seriesParallel + size_t enumeratedRowSubsets + size_t enumeratedColumnSubsets + + CMR_ERROR CMRbalancedParamsInit(CMR_BALANCED_PARAMS* params) + + CMR_ERROR CMRbalancedStatsInit(CMR_BALANCED_STATS* stats) + + # CMR_ERROR CMRbalancedStatsPrint(FILE* stream, CMR_BALANCED_STATS* stats, const char* prefix) + + CMR_ERROR CMRbalancedTest(CMR* cmr, CMR_CHRMAT* matrix, bool* pisBalanced, CMR_SUBMAT** psubmatrix, CMR_BALANCED_PARAMS* params, CMR_BALANCED_STATS* stats, double timeLimit) + + +# cdef extern from "cmr/block_decomposition.h": + +# ctypedef struct CMR_MATRIX + +# ctypedef struct CMR_BLOCK: +# CMR_MATRIX* matrix +# CMR_MATRIX* transpose +# size_t* rowsToOriginal +# size_t* columnsToOriginal + +# CMR_ERROR CMRdecomposeBlocks( +# CMR* cmr, +# CMR_MATRIX* matrix, +# size_t matrixType, +# size_t targetType, +# size_t* pnumBlocks, +# CMR_BLOCK** pblocks, +# size_t* rowsToBlock, +# size_t* columnsToBlock, +# size_t* rowsToBlockRows, +# size_t* columnsToBlockColumns +# ) + + +# Our global CMR environment +cdef CMR *cmr + +cdef CMR_CALL(CMR_ERROR _cmr_error) diff --git a/src/sage/libs/cmr/cmr.pyx b/src/sage/libs/cmr/cmr.pyx new file mode 100644 index 00000000000..f451c81c228 --- /dev/null +++ b/src/sage/libs/cmr/cmr.pyx @@ -0,0 +1,18 @@ +# sage_setup: distribution = sagemath-cmr +# -*- python -*- + +cdef CMR *cmr = NULL + + +cdef CMR_CALL(CMR_ERROR _cmr_error): + if _cmr_error == CMR_OKAY: + return + if _cmr_error == CMR_ERROR_INPUT: + raise RuntimeError("User input error") + if _cmr_error == CMR_ERROR_MEMORY: + raise RuntimeError("Memory (re)allocation failed") + if _cmr_error == CMR_ERROR_INVALID: + raise RuntimeError("Invalid input") + if _cmr_error == CMR_ERROR_TIMEOUT: + raise RuntimeError("Time limit exceeded") + raise RuntimeError("Unknown error") diff --git a/src/sage/matrix/all__sagemath_cmr.py b/src/sage/matrix/all__sagemath_cmr.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/sage/matrix/matrix_cmr_sparse.pxd b/src/sage/matrix/matrix_cmr_sparse.pxd new file mode 100644 index 00000000000..51b44895af1 --- /dev/null +++ b/src/sage/matrix/matrix_cmr_sparse.pxd @@ -0,0 +1,33 @@ +# sage_setup: distribution = sagemath-cmr +from sage.libs.cmr.cmr cimport CMR_CHRMAT, CMR_SEYMOUR_PARAMS, CMR_GRAPH, CMR_GRAPH_EDGE, bool + +from .matrix_sparse cimport Matrix_sparse + +cdef class Matrix_cmr_sparse(Matrix_sparse): + pass + +# cdef class Matrix_cmr_double_sparse(Matrix_cmr_sparse): +# pass + +# cdef class Matrix_cmr_int_sparse(Matrix_cmr_sparse): +# pass + +cdef class Matrix_cmr_chr_sparse(Matrix_cmr_sparse): + + cdef CMR_CHRMAT *_mat + cdef object _root + + cdef _init_from_dict(self, dict d, int nrows, int ncols, bint immutable=?) + + @staticmethod + cdef _from_cmr(CMR_CHRMAT *mat, bint immutable=?, base_ring=?) + +cdef _set_cmr_seymour_parameters(CMR_SEYMOUR_PARAMS *params, dict kwds) + +cdef _sage_edge(CMR_GRAPH *graph, CMR_GRAPH_EDGE e) +cdef _sage_edges(CMR_GRAPH *graph, CMR_GRAPH_EDGE *edges, int n, keys) +cdef _sage_graph(CMR_GRAPH *graph) + +cdef _sage_arc(CMR_GRAPH *graph, CMR_GRAPH_EDGE e, bint reversed) +cdef _sage_arcs(CMR_GRAPH *graph, CMR_GRAPH_EDGE *arcs, bool *arcs_reversed, n, keys) +cdef _sage_digraph(CMR_GRAPH *graph, bool *arcs_reversed) diff --git a/src/sage/matrix/matrix_cmr_sparse.pyx b/src/sage/matrix/matrix_cmr_sparse.pyx new file mode 100644 index 00000000000..47746555eed --- /dev/null +++ b/src/sage/matrix/matrix_cmr_sparse.pyx @@ -0,0 +1,4004 @@ +# sage_setup: distribution = sagemath-cmr +# sage.doctest: optional - sage.libs.cmr +r""" +Sparse Matrices from the Combinatorial Matrix Recognition Library + +This module is provided by the distribution :ref:`sagemath-cmr `. +""" + +# **************************************************************************** +# Copyright (C) 2023 Javier Santillan +# 2023-2024 Matthias Koeppe +# 2023-2024 Luze Xu +# +# 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/ +# **************************************************************************** + +from libc.stdint cimport SIZE_MAX + +from cysignals.memory cimport sig_malloc, sig_free +from cysignals.signals cimport sig_on, sig_off + +from sage.libs.cmr.cmr cimport * +from sage.rings.integer cimport Integer +from sage.rings.integer_ring import ZZ +from sage.structure.element cimport Matrix + +from .args cimport MatrixArgs_init +from .constructor import matrix +from .matrix_space import MatrixSpace +from .seymour_decomposition cimport create_DecompositionNode, GraphicNode + + +cdef class Matrix_cmr_sparse(Matrix_sparse): + r""" + Base class for sparse matrices implemented in CMR + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_sparse, Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 2, 3, sparse=True), + ....: [[1, 2, 3], [4, 0, 6]]) + sage: isinstance(M, Matrix_cmr_sparse) + True + """ + pass + + +cdef class Matrix_cmr_chr_sparse(Matrix_cmr_sparse): + r""" + Sparse matrices with 8-bit integer entries implemented in CMR + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 2, 3, sparse=True), + ....: [[1, 2, 3], [4, 0, 6]]); M + [1 2 3] + [4 0 6] + sage: M.dict() + {(0, 0): 1, (0, 1): 2, (0, 2): 3, (1, 0): 4, (1, 2): 6} + + Matrices of this class are always immutable:: + + sage: M.is_immutable() + True + sage: copy(M) is M + True + sage: deepcopy(M) is M + True + + This matrix class can only store very small integers:: + + sage: Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 1, 3, sparse=True), [-128, 0, 127]) + [-128 0 127] + sage: Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 1, 3, sparse=True), [126, 127, 128]) + Traceback (most recent call last): + ... + OverflowError: value too large to convert to char + + Arithmetic does not preserve the implementation class (even if the numbers would fit):: + + sage: M2 = M + M; M2 + [ 2 4 6] + [ 8 0 12] + sage: type(M2) + + sage: M * 100 + [100 200 300] + [400 0 600] + """ + def __init__(self, parent, entries=None, copy=None, bint coerce=True, immutable=True): + r""" + Create a sparse matrix with 8-bit integer entries implemented in CMR. + + INPUT: + + - ``parent`` -- a matrix space + + - ``entries`` -- see :func:`matrix` + + - ``copy`` -- ignored (for backwards compatibility) + + - ``coerce`` -- if False, assume without checking that the + entries lie in the base ring + + - ``immutable`` -- ignored (for backwards compatibility)? + + TESTS:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 2, 3, sparse=True), + ....: [[1, 2, 3], [4, 0, 6]]); M + [1 2 3] + [4 0 6] + sage: TestSuite(M).run() + """ + cdef dict d + ma = MatrixArgs_init(parent, entries) + d = ma.dict(coerce) + self._init_from_dict(d, ma.nrows, ma.ncols, immutable=True) + + cdef _init_from_dict(self, dict d, int nrows, int ncols, bint immutable=True): + r""" + Create ``self._mat`` (a ``CMR_CHRMAT``) from a dictionary via CMR functions. + + INPUT: + + - ``dict`` -- a dictionary of all nonzero elements. + + - ``nrows`` -- the number of rows. + + - ``ncols`` -- the number of columns. + + - ``immutable`` -- (boolean) make the matrix immutable. By default, + the output matrix is mutable. + """ + if cmr == NULL: + CMRcreateEnvironment(&cmr) + CMR_CALL(CMRchrmatCreate(cmr, &self._mat, nrows, ncols, len(d))) + for row in range(nrows): + self._mat.rowSlice[row] = 0 + for (row, col), coeff in d.items(): + if coeff: + self._mat.rowSlice[row + 1] += 1 + for row in range(nrows): + self._mat.rowSlice[row + 1] += self._mat.rowSlice[row] + for (row, col), coeff in d.items(): + if coeff: + index = self._mat.rowSlice[row] + self._mat.entryColumns[index] = col + self._mat.entryValues[index] = coeff + self._mat.rowSlice[row] = index + 1 + for row in reversed(range(nrows)): + self._mat.rowSlice[row + 1] = self._mat.rowSlice[row] + self._mat.rowSlice[0] = 0 + CMR_CALL(CMRchrmatSortNonzeros(cmr, self._mat)) + if immutable: + self.set_immutable() + + def matrix_from_rows_and_columns(self, rows, columns): + """ + Return the matrix constructed from ``self`` from the given rows and + columns. + + OUTPUT: A :class:`Matrix_cmr_chr_sparse` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = MatrixSpace(Integers(8),3,3) + sage: A = Matrix_cmr_chr_sparse(M, range(9)); A + [0 1 2] + [3 4 5] + [6 7 0] + sage: A.matrix_from_rows_and_columns([1], [0,2]) + [3 5] + sage: A.matrix_from_rows_and_columns([1,2], [1,2]) + [4 5] + [7 0] + + Note that row and column indices can be reordered:: + + sage: A.matrix_from_rows_and_columns([2,1], [2,1]) + [0 7] + [5 4] + + But the column indices can not be repeated:: + + sage: A.matrix_from_rows_and_columns([1,1,1],[2,0]) + [5 3] + [5 3] + [5 3] + sage: A.matrix_from_rows_and_columns([1,1,1],[2,0,0]) + Traceback (most recent call last): + ... + ValueError: The column indices can not be repeated + """ + if not isinstance(rows, (list, tuple)): + rows = list(rows) + + if not isinstance(columns, (list, tuple)): + columns = list(columns) + + if len(list(set(columns))) != len(columns): + raise ValueError("The column indices can not be repeated") + + if cmr == NULL: + CMRcreateEnvironment(&cmr) + + cdef CMR_SUBMAT *submatrix = NULL + cdef CMR_CHRMAT *cmr_submatrix = NULL + + try: + CMR_CALL(CMRsubmatCreate(cmr, len(rows), len(columns), &submatrix)) + + for i in range(submatrix.numRows): + submatrix.rows[i] = rows[i] + + for j in range(submatrix.numColumns): + submatrix.columns[j] = columns[j] + + CMR_CALL(CMRchrmatSlice(cmr, self._mat, submatrix, &cmr_submatrix)) + finally: + CMR_CALL(CMRsubmatFree(cmr, &submatrix)) + + return Matrix_cmr_chr_sparse._from_cmr(cmr_submatrix) + + cdef get_unsafe(self, Py_ssize_t i, Py_ssize_t j): + """ + Return ``(i, j)`` entry of this matrix as an integer. + + .. warning:: + + This is very unsafe; it assumes ``i`` and ``j`` are in the right + range. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 2, 3, sparse=True), + ....: [[1, 2, 3], [4, 0, 6]]); M + [1 2 3] + [4 0 6] + sage: M[1, 2] + 6 + sage: M[1, 3] + Traceback (most recent call last): + ... + IndexError: matrix index out of range + sage: M[-1, 0] + 4 + """ + cdef size_t index + CMR_CALL(CMRchrmatFindEntry(self._mat, i, j, &index)) + if index == SIZE_MAX: + return Integer(0) + return Integer(self._mat.entryValues[index]) + + def _dict(self): + """ + Return the underlying dictionary of ``self``. + """ + cdef dict d + d = {} + for row in range(self._mat.numRows): + for index in range(self._mat.rowSlice[row], self._mat.rowSlice[row + 1]): + d[(row, self._mat.entryColumns[index])] = Integer(self._mat.entryValues[index]) + return d + + def __copy__(self): + """ + Matrix_cmr_chr_sparse matrices are immutable. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 2, 3, sparse=True), + ....: [[1, 2, 3], [4, 0, 6]]) + sage: copy(M) is M + True + """ + return self + + def __deepcopy__(self, memo): + """ + Matrix_cmr_chr_sparse matrices are immutable. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 2, 3, sparse=True), + ....: [[1, 2, 3], [4, 0, 6]]) + sage: deepcopy(M) is M + True + """ + return self + + def __dealloc__(self): + """ + Frees all the memory allocated for this matrix. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 2, 3, sparse=True), + ....: [[1, 2, 3], [4, 0, 6]]) + sage: del M + """ + if self._root is None or self._root is self: + # We own it, so we have to free it. + CMR_CALL(CMRchrmatFree(cmr, &self._mat)) + + def _test_change_ring(self, **options): + return + + def _pickle(self): + """ + Utility function for pickling. + + TESTS:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 2, 3, sparse=True), + ....: [[1, 2, 3], [4, 0, 6]]) + sage: loads(dumps(M)) == M + True + """ + version = 0 + return self._dict(), version + + def _unpickle(self, data, int version): + if version != 0: + raise RuntimeError("unknown matrix version (=%s)"%version) + self._init_from_dict(data, self._nrows, self._ncols) + + # CMR-specific methods. Other classes that want to provide these methods should create + # a copy of themselves as an instance of this class and delegate to it. + + @staticmethod + def _from_data(data, immutable=True): + """ + Create a matrix (:class:`Matrix_cmr_chr_sparse`) from data or a matrix. + + INPUT: + + - ``data`` -- a matrix or data to construct a matrix. + + - ``immutable`` -- (boolean) make the matrix immutable. By default, + the output matrix is mutable. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse._from_data([[1, 2, 3], [4, 0, 6]]); M + [1 2 3] + [4 0 6] + sage: N = matrix(ZZ, [[1, 2, 3], [4, 0, 6]]) + sage: Matrix_cmr_chr_sparse._from_data(N) + [1 2 3] + [4 0 6] + """ + if not isinstance(data, Matrix): + data = matrix(ZZ, data, sparse=True) + if not isinstance(data, Matrix_cmr_chr_sparse): + data = Matrix_cmr_chr_sparse(data.parent(), data, immutable=immutable) + return data + + @staticmethod + cdef _from_cmr(CMR_CHRMAT *mat, bint immutable=False, base_ring=None): + r""" + INPUT: + + - ``mat`` -- a ``CMR_CHRMAT``; after this call, it is owned by the created Python object + + OUTPUT: A :class:`Matrix_cmr_chr_sparse` + + """ + cdef Matrix_cmr_chr_sparse result + if base_ring is None: + base_ring = ZZ + ms = MatrixSpace(base_ring, mat.numRows, mat.numColumns, sparse=True) + result = Matrix_cmr_chr_sparse.__new__(Matrix_cmr_chr_sparse, ms, immutable=immutable) + result._mat = mat + result._root = None + return result + + @staticmethod + def _network_matrix_from_digraph(digraph, forest_arcs=None, arcs=None, vertices=None): + r""" + Return the network matrix of ``digraph``, pivoted according to ``forest_arcs``. + + Its rows are indexed parallel to ``forest_arcs``. + It is in "short tableau" form, i.e., the columns are indexed parallel + to the elements of ``arcs`` that are not in ``forest_arcs``. + + .. NOTE:: + + In [Sch1986]_, the columns are indexed by all arcs of the digraph, + giving a "long tableau" form of the network matrix. + + INPUT: + + - ``digraph`` -- a :class:`DiGraph` + + - ``forest_arcs`` -- a sequence of arcs of the ``digraph`` or ``None`` (the default: + use the labels of the ``arcs`` as a boolean value) + + - ``arcs`` -- a sequence of arcs of the digraph or ``None`` (the default: + all arcs going out from the ``vertices``) + + - ``vertices`` -- a sequence of vertices of the digraph or ``None`` (the default: + all vertices of the ``digraph``) + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + + Defining the forest by arc labels:: + + sage: D = DiGraph([[0, 1, 2, 3], + ....: [(0, 1, True), (0, 2, True), (1, 2), (1, 3, True), (2, 3)]]) + sage: M = Matrix_cmr_chr_sparse._network_matrix_from_digraph(D); M + [ 1 -1] + [-1 1] + [ 1 0] + + Defining the forest by a separate list of forest arcs:: + + sage: D = DiGraph([[0, 1, 2, 3], + ....: [(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)]]) + sage: T = [(0, 1), (0, 2), (1, 3)] + sage: M = Matrix_cmr_chr_sparse._network_matrix_from_digraph(D, T); M + [ 1 -1] + [-1 1] + [ 1 0] + + Prescribing an order for the arcs (columns):: + + sage: D = DiGraph([[0, 1, 2, 3], + ....: [(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)]]) + sage: T = [(0, 1), (0, 2), (1, 3)] + sage: A = [(2, 3), (0, 1), (0, 2), (1, 2), (1, 3)] + sage: M = Matrix_cmr_chr_sparse._network_matrix_from_digraph(D, T, arcs=A); M + [ 1 -1] + [-1 1] + [ 1 0] + + TESTS:: + + sage: D = DiGraph([[0, 1, 2], [(0, 1), (1, 2), (0, 2)]]) + sage: not_a_forest = [(0, 1), (1, 2), (0, 2)] + sage: M = Matrix_cmr_chr_sparse._network_matrix_from_digraph(D, not_a_forest); M + Traceback (most recent call last): + ... + ValueError: not a spanning forest + """ + cdef CMR_GRAPH *cmr_digraph = NULL + cdef dict vertex_to_cmr_node = {} + cdef dict arc_to_cmr_edge = {} + cdef CMR_GRAPH_EDGE cmr_edge + cdef CMR_GRAPH_NODE cmr_node + + if cmr == NULL: + CMRcreateEnvironment(&cmr) + + CMR_CALL(CMRgraphCreateEmpty(cmr, &cmr_digraph, digraph.num_verts(), digraph.num_edges())) + + if vertices is None: + iter_vertices = digraph.vertex_iterator() + else: + iter_vertices = vertices + for u in iter_vertices: + CMR_CALL(CMRgraphAddNode(cmr, cmr_digraph, &cmr_node)) + vertex_to_cmr_node[u] = cmr_node + + vertices = vertex_to_cmr_node.keys() + if arcs is None: + arcs = digraph.edge_iterator(labels=False, vertices=vertices, ignore_direction=False) + + for a in arcs: + u, v = a + CMR_CALL(CMRgraphAddEdge(cmr, cmr_digraph, vertex_to_cmr_node[u], + vertex_to_cmr_node[v], &cmr_edge)) + arc_to_cmr_edge[(u, v)] = cmr_edge + + cdef CMR_GRAPH_EDGE *cmr_forest_arcs = NULL + cdef bool *cmr_arcs_reversed = NULL + cdef CMR_CHRMAT *cmr_matrix = NULL + cdef bool is_correct_forest + cdef size_t num_forest_arcs + + cdef size_t mem_arcs + if forest_arcs is not None: + mem_arcs = len(forest_arcs) + else: + mem_arcs = len(vertices) - 1 + + CMR_CALL(_CMRallocBlockArray(cmr, &cmr_forest_arcs, mem_arcs, sizeof(CMR_GRAPH_EDGE))) + try: + if forest_arcs is None: + num_forest_arcs = 0 + for u, v, label in digraph.edge_iterator(labels=True, vertices=vertices, + ignore_direction=False): + if label: + if num_forest_arcs >= mem_arcs: + raise ValueError('not a spanning forest') + cmr_forest_arcs[num_forest_arcs] = arc_to_cmr_edge[(u, v)] + num_forest_arcs += 1 + else: + num_forest_arcs = len(forest_arcs) + for i, (u, v) in enumerate(forest_arcs): + cmr_forest_arcs[i] = arc_to_cmr_edge[(u, v)] + + CMR_CALL(_CMRallocBlockArray(cmr, &cmr_arcs_reversed, len(arc_to_cmr_edge), sizeof(bool))) + try: + for j in range(len(arc_to_cmr_edge)): + cmr_arcs_reversed[j] = False + CMR_CALL(CMRnetworkComputeMatrix(cmr, cmr_digraph, &cmr_matrix, NULL, + cmr_arcs_reversed, num_forest_arcs, cmr_forest_arcs, + 0, NULL, &is_correct_forest)) + if not is_correct_forest: + raise ValueError('not a spanning forest') + finally: + CMR_CALL(_CMRfreeBlockArray(cmr, &cmr_arcs_reversed)) + finally: + CMR_CALL(_CMRfreeBlockArray(cmr, &cmr_forest_arcs)) + + return Matrix_cmr_chr_sparse._from_cmr(cmr_matrix) + + @staticmethod + def one_sum(*summands): + r""" + Return a block-diagonal matrix constructed from the given matrices (summands). + + INPUT: + + - ``summands`` -- integer matrices or data from which integer matrices can be + constructed + + OUTPUT: A :class:`Matrix_cmr_chr_sparse` + + The terminology "1-sum" is used in the context of Seymour's decomposition + of totally unimodular matrices and regular matroids, see [Sch1986]_. + + .. SEEALSO:: :meth:`two_sum`, :meth:`three_sum` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse.one_sum([[1, 0], [-1, 1]], [[1, 1], [-1, 0]]) + sage: unicode_art(M) + ⎛ 1 0│ 0 0⎞ + ⎜-1 1│ 0 0⎟ + ⎜─────┼─────⎟ + ⎜ 0 0│ 1 1⎟ + ⎝ 0 0│-1 0⎠ + sage: M = Matrix_cmr_chr_sparse.one_sum([[1, 0], [-1, 1]], [[1]], [[-1]]) + sage: unicode_art(M) + ⎛ 1 0│ 0│ 0⎞ + ⎜-1 1│ 0│ 0⎟ + ⎜─────┼──┼──⎟ + ⎜ 0 0│ 1│ 0⎟ + ⎜─────┼──┼──⎟ + ⎝ 0 0│ 0│-1⎠ + + TESTS:: + + sage: M = Matrix_cmr_chr_sparse.one_sum(); M + [] + sage: M.parent() + Full MatrixSpace of 0 by 0 sparse matrices over Integer Ring + + sage: M = Matrix_cmr_chr_sparse.one_sum([[1, 0], [-1, 1]]); unicode_art(M) + ⎛ 1 0⎞ + ⎝-1 1⎠ + sage: M.parent() + Full MatrixSpace of 2 by 2 sparse matrices over Integer Ring + """ + cdef Matrix_cmr_chr_sparse sum, summand + cdef CMR_CHRMAT *sum_mat + summands = iter(summands) + try: + first = next(summands) + except StopIteration: + return Matrix_cmr_chr_sparse._from_data({}) + sum = Matrix_cmr_chr_sparse._from_data(first, immutable=False) + sum_mat = sum._mat + row_subdivision = [] + column_subdivision = [] + + cdef CMR_CHRMAT *tmp_sum_mat = NULL + sig_on() + try: + for s in summands: + row_subdivision.append(sum_mat.numRows) + column_subdivision.append(sum_mat.numColumns) + summand = Matrix_cmr_chr_sparse._from_data(s) + CMR_CALL(CMRoneSum(cmr, sum_mat, summand._mat, &tmp_sum_mat)) + sum_mat = tmp_sum_mat + tmp_sum_mat = NULL + + finally: + sig_off() + + if sum_mat != sum._mat: + sum = Matrix_cmr_chr_sparse._from_cmr(sum_mat, immutable=False) + if row_subdivision or column_subdivision: + sum.subdivide(row_subdivision, column_subdivision) + sum.set_immutable() + return sum + + def two_sum(first_mat, second_mat, first_index, second_index, nonzero_block="top_right"): + r""" + Return the 2-sum matrix constructed from the given matrices ``first_mat`` and + ``second_mat``, with index of the first matrix ``first_index`` and index + of the second matrix ``second_index``. + Suppose that ``first_index`` indicates the last column of ``first_mat`` and + ``second_index`` indicates the first row of ``second_mat``, + i.e, the first matrix is `M_1=\begin{bmatrix} A & a\end{bmatrix}` + and the second matrix is `M_2=\begin{bmatrix} b^T \\ B\end{bmatrix}`. Then + the two sum `M_1 \oplus_2 M_2 =\begin{bmatrix}A & ab^T\\ 0 & B\end{bmatrix}`. + + The terminology "2-sum" is used in the context of Seymour's decomposition + of totally unimodular matrices and regular matroids, see [Sch1986]_, Ch. 19.4. + + .. SEEALSO:: :meth:`one_sum`, :meth:`three_sum` + + INPUT: + + - ``first_mat`` -- the first integer matrix + - ``second_mat`` -- the second integer matrix + - ``first_index`` -- the column/row index of the first integer matrix + - ``second_index`` -- the row/column index of the second integer matrix + - ``nonzero_block`` -- either ``"top_right"`` (default) or ``"bottom_left"``; + whether the nonzero block in the 2-sum matrix locates in the top right or bottom left. + If ``nonzero_block="top_right"``, + ``first_index`` is the column index of the first integer matrix, + ``second_index`` is the row index of the second integer matrix. + The outer product of the corresponding column and row + form the nonzero top right block of the 2-sum matrix. + If ``nonzero_block="bottom_left"``, + ``first_index`` is the row index of the first integer matrix, + ``second_index`` is the column index of the second integer matrix. + The outer product of the corresponding row and column + form the nonzero bottom left block of the 2-sum matrix. + + OUTPUT: A :class:`Matrix_cmr_chr_sparse` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M1 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 2, 3, sparse=True), + ....: [[1, 2, 3], [4, 5, 6]]); M1 + [1 2 3] + [4 5 6] + sage: M2 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 2, 3, sparse=True), + ....: [[7, 8, 9], [-1, -2, -3]]); M2 + [ 7 8 9] + [-1 -2 -3] + sage: Matrix_cmr_chr_sparse.two_sum(M1, M2, 2, 0) + [ 1 2|21 24 27] + [ 4 5|42 48 54] + [-----+--------] + [ 0 0|-1 -2 -3] + sage: M1.two_sum(M2, 1, 1) + [ 1 3| -2 -4 -6] + [ 4 6| -5 -10 -15] + [-------+-----------] + [ 0 0| 7 8 9] + sage: M1.two_sum(M2, 1, 1, nonzero_block="bottom_right") + Traceback (most recent call last): + ... + ValueError: ('Unknown two sum mode', 'bottom_right') + sage: M1.two_sum(M2, 1, 1, nonzero_block="bottom_left") + [ 1 2 3| 0 0] + [-----------+-------] + [ 32 40 48| 7 9] + [ -8 -10 -12| -1 -3] + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M1 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 5, sparse=True), + ....: [[1, 1, 0, 0, 0], + ....: [1, 0, 1,-1, 1], + ....: [0,-1, 1, 0,-1], + ....: [0, 0,-1, 1, 0], + ....: [0, 1, 1, 0, 1]]); M1 + [ 1 1 0 0 0] + [ 1 0 1 -1 1] + [ 0 -1 1 0 -1] + [ 0 0 -1 1 0] + [ 0 1 1 0 1] + sage: M2 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 5, sparse=True), + ....: [[1,-1, 1, 0, 0], + ....: [1, 1, 1, 1,-1], + ....: [0, 0,-1, 0, 1], + ....: [1, 0, 0,-1, 0], + ....: [0, 1, 0, 0, 1]]); M2 + [ 1 -1 1 0 0] + [ 1 1 1 1 -1] + [ 0 0 -1 0 1] + [ 1 0 0 -1 0] + [ 0 1 0 0 1] + sage: M1.two_sum(M2, 1, 2, nonzero_block="bottom_left") + [ 1 1 0 0 0| 0 0 0 0] + [ 0 -1 1 0 -1| 0 0 0 0] + [ 0 0 -1 1 0| 0 0 0 0] + [ 0 1 1 0 1| 0 0 0 0] + [--------------+-----------] + [ 1 0 1 -1 1| 1 -1 0 0] + [ 1 0 1 -1 1| 1 1 1 -1] + [-1 0 -1 1 -1| 0 0 0 1] + [ 0 0 0 0 0| 1 0 -1 0] + [ 0 0 0 0 0| 0 1 0 1] + sage: M1.two_sum(M2, 4, 0) + [ 1 1 0 0| 0 0 0 0 0] + [ 1 0 1 -1| 1 -1 1 0 0] + [ 0 -1 1 0|-1 1 -1 0 0] + [ 0 0 -1 1| 0 0 0 0 0] + [ 0 1 1 0| 1 -1 1 0 0] + [-----------+--------------] + [ 0 0 0 0| 1 1 1 1 -1] + [ 0 0 0 0| 0 0 -1 0 1] + [ 0 0 0 0| 1 0 0 -1 0] + [ 0 0 0 0| 0 1 0 0 1] + + TESTS:: + + sage: M1 = Matrix_cmr_chr_sparse(MatrixSpace(GF(3), 2, 3, sparse=True), + ....: [[1, 2, 3], [4, 5, 6]]); M1 + [1 2 0] + [1 2 0] + sage: M2 = Matrix_cmr_chr_sparse(MatrixSpace(GF(5), 2, 3, sparse=True), + ....: [[7, 8, 9], [-1, -2, -3]]); M2 + [2 3 4] + [4 3 2] + sage: Matrix_cmr_chr_sparse.two_sum(M1, M2, 2, 0) + Traceback (most recent call last): + ... + ValueError: summands must have the same base ring, + got Finite Field of size 3, Finite Field of size 5 + """ + cdef Matrix_cmr_chr_sparse sum, first, second + cdef CMR_CHRMAT *sum_mat = NULL + first = Matrix_cmr_chr_sparse._from_data(first_mat) + second = Matrix_cmr_chr_sparse._from_data(second_mat) + first_base_ring = first.parent().base_ring() + second_base_ring = second.parent().base_ring() + if first_base_ring != second_base_ring: + raise ValueError(f'summands must have the same base ring, ' + f'got {first_base_ring}, {second_base_ring}') + + if nonzero_block not in ["top_right", "bottom_left"]: + raise ValueError("Unknown two sum mode", nonzero_block) + + if nonzero_block == "top_right": + column = first_index + row = second_index + if column < 0 or column >= first._mat.numColumns: + raise ValueError("First marker should be a column index of the first matrix") + if row < 0 or row >= second._mat.numRows: + raise ValueError("Second marker should be a row index of the second matrix") + row_subdivision = [] + column_subdivision = [] + row_subdivision.append(first._mat.numRows) + column_subdivision.append(first._mat.numColumns - 1) + first_marker = CMRcolumnToElement(column) + second_marker = CMRrowToElement(row) + else: + row = first_index + column = second_index + if row < 0 or row >= first._mat.numRows: + raise ValueError("First marker should be a Row index of the first matrix") + if column < 0 or column >= second._mat.numColumns: + raise ValueError("Second marker should be a column index of the second matrix") + row_subdivision = [] + column_subdivision = [] + row_subdivision.append(first._mat.numRows - 1) + column_subdivision.append(first._mat.numColumns) + first_marker = CMRrowToElement(row) + second_marker = CMRcolumnToElement(column) + + cdef int8_t characteristic = first_base_ring.characteristic() + + sig_on() + try: + CMR_CALL(CMRtwoSum(cmr, first._mat, second._mat, first_marker, second_marker, characteristic, &sum_mat)) + finally: + sig_off() + + sum = Matrix_cmr_chr_sparse._from_cmr(sum_mat, immutable=False, base_ring=first_base_ring) + if row_subdivision or column_subdivision: + sum.subdivide(row_subdivision, column_subdivision) + sum.set_immutable() + return sum + + def _three_sum_cmr(first_mat, second_mat, + first_index1, first_index2, second_index1, second_index2, + three_sum_strategy="distributed_ranks"): + r""" + Return the 3-sum matrix constructed from the two matrices + ``first_mat`` and ``second_index`` via + ``first_index1``, ``first_index2``, ``second_index1``, ``second_index2``. + + Suppose that ``three_sum_strategy="distributed_ranks"``. + If ``first_index1`` indexes a row vector `a_1^T` and + ``first_index2`` indexes a column vector `a_2` of ``first_mat``, + then ``second_index1`` indexes a column vector `b_1` and + ``second_index2`` indexes a row vector `b_2` of ``second_mat``. + In this case, + the first matrix is + `M_1=\begin{bmatrix} A & a_2 \\ a_1^T & *\end{bmatrix}` + and the second matrix is + `M_2=\begin{bmatrix} * & b_2^T\\ b_1 & B\end{bmatrix}`, + where the entry `*` is not relevant in the construction. + Then the Seymour/Schrijver 3-sum is the matrix + `M_1 \oplus_3 M_2 = \begin{bmatrix} A & a_2 b_2^T \\ b_1 a_1^T & B\end{bmatrix}`. + + Suppose that ``three_sum_strategy="concentrated_rank"``. + If ``first_index1`` and ``first_index2`` both index row vectors + `a_1^T, a_2^T` of ``first_mat``, + then ``second_index1`` and ``second_index2`` must index column vectors + `b_1, b_2` of ``second_mat``. In this case, + the first matrix is + `M_1=\begin{bmatrix} A \\ a_1^T \\ a_2^T \end{bmatrix}` + and the second matrix is + `M_2=\begin{bmatrix} b_1 & b_2 & B\end{bmatrix}`. + Then the Truemper 3-sum is the matrix + `M_1 \oplus_3 M_2 = \begin{bmatrix} A & 0 \\ C & B\end{bmatrix}`, + where `\begin{bmatrix}a_1^T \\ a_2^T\end{bmatrix}=\begin{bmatrix} C_1 & \bar{C}\end{bmatrix}`, + `\begin{bmatrix}b_1 & b_2\end{bmatrix}=\begin{bmatrix} \bar{C}\\ C_2\end{bmatrix}`, + `C_12 = C_2 \bar{C}^{-1} C_1`, + `C=\begin{bmatrix} C_1 & \bar{C} \\ C_{12} & C_2\end{bmatrix}`, i.e., + `C=\begin{bmatrix}b_1 & b_2\end{bmatrix}\bar{C}^{-1}\begin{bmatrix}a_1^T\\ a_2^T\end{bmatrix}` + + The terminology "3-sum" is used in the context of Seymour's decomposition + of totally unimodular matrices and regular matroids, see [Sch1986]_, Ch. 19.4. + + .. SEEALSO:: :meth:`one_sum`, :meth:`two_sum`, :meth:`three_sum`, + :meth:`three_sum_wide_wide`, :meth:`three_sum_mixed_mixed` + + INPUT: + + - ``first_mat`` -- the first integer matrix + - ``second_mat`` -- the second integer matrix + - ``first_index1`` -- the column/row index of the first integer matrix + - ``first_index2`` -- the column/row index of the first integer matrix + - ``second_index1`` -- the row/column index of the second integer matrix + - ``second_index2`` -- the row/column index of the second integer matrix + - ``three_sum_strategy`` -- either ``"distributed_ranks"`` (default) + or ``"concentrated_rank"``. + + OUTPUT: A :class:`Matrix_cmr_chr_sparse` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M1 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 5, sparse=True), + ....: [[1, 1, 0, 0, 0], + ....: [1, 0, 1,-1, 1], + ....: [0,-1, 1, 0,-1], + ....: [0, 0,-1, 1, 0], + ....: [0, 1, 1, 0, 1]]); M1 + [ 1 1 0 0 0] + [ 1 0 1 -1 1] + [ 0 -1 1 0 -1] + [ 0 0 -1 1 0] + [ 0 1 1 0 1] + sage: M2 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 5, sparse=True), + ....: [[1,-1, 1, 0, 0], + ....: [1, 1, 1, 1,-1], + ....: [0, 0,-1, 0, 1], + ....: [1, 0, 0,-1, 0], + ....: [0, 1, 0, 0, 1]]); M2 + [ 1 -1 1 0 0] + [ 1 1 1 1 -1] + [ 0 0 -1 0 1] + [ 1 0 0 -1 0] + [ 0 1 0 0 1] + sage: M1._three_sum_cmr(M2, 1, 2, 2, 4, three_sum_strategy="concentrated_rank") + [ 1 1 0 0 0 0 0 0] + [ 0 0 -1 1 0 0 0 0] + [ 0 1 1 0 1 0 0 0] + [ 1 0 1 -1 1 1 -1 0] + [ 1 1 0 -1 2 1 1 1] + [-1 -1 0 1 -2 0 0 0] + [ 0 0 0 0 0 1 0 -1] + [ 0 -1 1 0 -1 0 1 0] + sage: M1._three_sum_cmr(M2, 1, 2, 1, 1) + [ 1 1 0 0 0 0 0 0] + [ 0 -1 0 -1 1 1 1 -1] + [ 0 0 1 0 -1 -1 -1 1] + [ 0 1 0 1 1 1 1 -1] + [-1 0 1 -1 1 1 0 0] + [ 0 0 0 0 0 -1 0 1] + [ 0 0 0 0 1 0 -1 0] + [ 1 0 -1 1 0 0 0 1] + """ + cdef Matrix_cmr_chr_sparse sum, first, second + cdef CMR_CHRMAT *sum_mat = NULL + first = Matrix_cmr_chr_sparse._from_data(first_mat, immutable=False) + second = Matrix_cmr_chr_sparse._from_data(second_mat, immutable=False) + + if three_sum_strategy not in ["distributed_ranks", "concentrated_rank"]: + raise ValueError("Unknown three sum mode", three_sum_strategy) + + if three_sum_strategy == "distributed_ranks": + row1 = first_index1 + column2 = first_index2 + column1 = second_index1 + row2 = second_index2 + if row1 < 0 or row1 >= first._mat.numRows: + raise ValueError("First marker 1 should be a row index of the first matrix") + if column2 < 0 or column2 >= first._mat.numColumns: + raise ValueError("First marker 2 should be a column index of the first matrix") + if column1 < 0 or column1 >= second._mat.numColumns: + raise ValueError("Second marker 1 should be a column index of the second matrix") + if row2 < 0 or row2 >= second._mat.numRows: + raise ValueError("Second marker 2 should be a row index of the second matrix") + first_marker1 = CMRrowToElement(row1) + first_marker2 = CMRcolumnToElement(column2) + second_marker1 = CMRcolumnToElement(column1) + second_marker2 = CMRrowToElement(row2) + else: + row1 = first_index1 + row2 = first_index2 + column1 = second_index1 + column2 = second_index2 + if row1 < 0 or row1 >= first._mat.numRows: + raise ValueError("First marker 1 should be a row index of the first matrix") + if row2 < 0 or row2 >= first._mat.numRows: + raise ValueError("First marker 2 should be a row index of the first matrix") + if column1 < 0 or column1 >= second._mat.numColumns: + raise ValueError("Second marker 1 should be a column index of the second matrix") + if column2 < 0 or column2 >= second._mat.numColumns: + raise ValueError("Second marker 2 should be a column index of the second matrix") + first_marker1 = CMRrowToElement(row1) + first_marker2 = CMRrowToElement(row2) + second_marker1 = CMRcolumnToElement(column1) + second_marker2 = CMRcolumnToElement(column2) + + cdef int8_t characteristic = first_mat.parent().characteristic() + + if second_mat.parent().characteristic() != characteristic: + raise ValueError("The characteristic of two matrices are different") + + sig_on() + try: + CMR_CALL(CMRthreeSum(cmr, first._mat, second._mat, first_marker1, second_marker1, first_marker2, second_marker2, characteristic, &sum_mat)) + finally: + sig_off() + + sum = Matrix_cmr_chr_sparse._from_cmr(sum_mat) + return sum + + def three_sum_wide_wide(first_mat, second_mat, + first_row_index=-1, + first_columns_index=[-2, -1], + second_row_index=0, + second_columns_index=[0, 1], + algorithm="cmr", + verify=True, + sign_verify=False): + r""" + Return the 3-sum matrix constructed from the given matrices ``first_mat`` and + ``second_mat``, assume that ``three_sum_strategy="distributed_ranks"``. + The first matrix is + `M_1=\begin{bmatrix} A & a_2 & a_2\\ a_1^T & 0 & 1\end{bmatrix}` + and the second matrix is + `M_2=\begin{bmatrix} 1 & 0 & b_2^T\\ b_1 & b_1 & B\end{bmatrix}`. + Then the Seymour/Schrijver 3-sum is the matrix + `M_1 \oplus_3 M_2 = \begin{bmatrix} A & a_2 b_2^T \\ b_1 a_1^T & B\end{bmatrix}`. + + The terminology "3-sum" is used in the context of Seymour's decomposition + of totally unimodular matrices and regular matroids, see [Sch1986]_, Ch. 19.4. + + .. SEEALSO:: :meth:`three_sum`, :meth:`is_three_sum`, + :meth:`three_sum_mixed_mixed` + + INPUT: + + - ``first_mat`` -- the first integer matrix `M_1` + - ``second_mat`` -- the second integer matrix `M_2` + - ``first_row_index`` -- the row index of `a_1^T` in `M_1` + - ``first_columns_index`` -- the column indices of `a_2` in `M_1` + - ``second_row_index`` -- the row index of `b_2^T` in `M_2` + - ``second_columns_index`` -- the column indices of `b_1` in `M_2` + - ``algorithm`` -- ``"cmr"`` or ``"direct"`` + If ``algorithm="cmr"``, then use :meth:`_three_sum_cmr`; + If ``algorithm="direct"``, then construct three sum directly. + - ``verify`` -- boolean (default: ``True``); + whether to check the given two matrices and the related indices + satisfying the requirements of 3-sum + by calling :meth:`is_three_sum_wide_wide`. + - ``sign_verify`` -- boolean (default: ``False``); + whether to check the sign correctness. + See :meth:`is_three_sum_wide_wide`. + + OUTPUT: A :class:`Matrix_cmr_chr_sparse` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M1 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 6, sparse=True), + ....: [[1, 1, 0, 0, 0, 0], + ....: [0,-1, 0,-1, 1, 1], + ....: [0, 0, 1, 0,-1,-1], + ....: [0, 1, 0, 1, 1, 1], + ....: [1, 0,-1, 1, 0, 1],]); M1 + [ 1 1 0 0 0 0] + [ 0 -1 0 -1 1 1] + [ 0 0 1 0 -1 -1] + [ 0 1 0 1 1 1] + [ 1 0 -1 1 0 1] + sage: M2 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 6, sparse=True), + ....: [[ 1, 0, 1, 1, 1,-1], + ....: [-1,-1, 1, 1, 0, 0], + ....: [ 0, 0, 0,-1, 0, 1], + ....: [ 0, 0, 1, 0,-1, 0], + ....: [ 1, 1, 0, 0, 0, 1]]); M2 + [ 1 0 1 1 1 -1] + [-1 -1 1 1 0 0] + [ 0 0 0 -1 0 1] + [ 0 0 1 0 -1 0] + [ 1 1 0 0 0 1] + sage: Matrix_cmr_chr_sparse.three_sum_wide_wide(M1, M2) + [ 1 1 0 0 0 0 0 0] + [ 0 -1 0 -1 1 1 1 -1] + [ 0 0 1 0 -1 -1 -1 1] + [ 0 1 0 1 1 1 1 -1] + [-1 0 1 -1 1 1 0 0] + [ 0 0 0 0 0 -1 0 1] + [ 0 0 0 0 1 0 -1 0] + [ 1 0 -1 1 0 0 0 1] + + Three sum can be computed for any row and column indices: + + sage: M1 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 6, sparse=True), + ....: [[1, 1, 0, 0, 0, 0], + ....: [1, 0, 1,-1, 1, 0], + ....: [0,-1, 1, 0,-1, 1], + ....: [0, 0,-1, 1, 0,-1], + ....: [0, 1, 1, 0, 1, 1]]); M1 + [ 1 1 0 0 0 0] + [ 1 0 1 -1 1 0] + [ 0 -1 1 0 -1 1] + [ 0 0 -1 1 0 -1] + [ 0 1 1 0 1 1] + sage: M2 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 6, sparse=True), + ....: [[1,-1, 1, 0, 0,-1], + ....: [1, 1, 1, 1,-1, 0], + ....: [0, 0,-1, 0, 1, 0], + ....: [1, 0, 0,-1, 0, 0], + ....: [0, 1, 0, 0, 1, 1]]); M2 + [ 1 -1 1 0 0 -1] + [ 1 1 1 1 -1 0] + [ 0 0 -1 0 1 0] + [ 1 0 0 -1 0 0] + [ 0 1 0 0 1 1] + sage: M = M1.three_sum_wide_wide(M2, 1, [2,-1], 1, [1, -1]); M + [ 1 1 0 0 0 0 0 0] + [ 0 -1 0 -1 1 1 1 -1] + [ 0 0 1 0 -1 -1 -1 1] + [ 0 1 0 1 1 1 1 -1] + [-1 0 1 -1 1 1 0 0] + [ 0 0 0 0 0 -1 0 1] + [ 0 0 0 0 1 0 -1 0] + [ 1 0 -1 1 0 0 0 1] + sage: N = M1.three_sum_wide_wide(M2, 1, [2,-1], 1, [1, -1], algorithm="direct") + sage: M == N + True + + sage: M1.three_sum_wide_wide(M2, 1, [2, 3], 1, [1, -1]) + Traceback (most recent call last): + ... + ValueError: The given two matrices and related indices do not satisfy the rule for three sum! + sage: M1.three_sum_wide_wide(M2, 1, [2, 3], 1, [1, -1], verify=False) + [ 1 1 0 0 0 0 0 0] + [ 0 -1 -1 1 1 1 1 -1] + [ 0 0 0 -1 -1 -1 -1 1] + [ 0 1 1 1 1 1 1 -1] + [-1 0 -1 0 1 1 0 0] + [ 0 0 0 0 0 -1 0 1] + [ 0 0 0 0 1 0 -1 0] + [ 1 0 1 0 0 0 0 1] + """ + m1 = first_mat.nrows() + n1 = first_mat.ncols() + m2 = second_mat.nrows() + n2 = second_mat.ncols() + j1 = first_columns_index[0] + j2 = first_columns_index[1] + j1 = j1 if j1 >= 0 else n1 + j1 + j2 = j2 if j2 >= 0 else n1 + j2 + i1 = first_row_index + i1 = i1 if i1 >= 0 else m1 + i1 + k1 = second_columns_index[0] + k2 = second_columns_index[1] + k1 = k1 if k1 >= 0 else n2 + k1 + k2 = k2 if k2 >= 0 else n2 + k2 + i2 = second_row_index + i2 = i2 if i2 >= 0 else m2 + i2 + if j1 > j2: + j2, j1 = j1, j2 + if k1 > k2: + k2, k1 = k1, k2 + + if algorithm not in ["cmr", "direct"]: + raise ValueError("Unknown algorithm", algorithm) + + if algorithm == "cmr": + column_index_1 = [j for j in range(n1) if j != j2] + column_index_2 = [j for j in range(n2) if j != k2] + first_submat = first_mat.matrix_from_rows_and_columns(range(m1), column_index_1) + second_submat = second_mat.matrix_from_rows_and_columns(range(m2), column_index_2) + M = Matrix_cmr_chr_sparse._three_sum_cmr(first_submat, second_submat, + i1, j1, + i2, k1, + three_sum_strategy="distributed_ranks") + if algorithm == "direct": + row_index_1 = [i for i in range(m1) if i != i1] + column_index_1 = [j for j in range(n1) if j != j1 and j != j2] + row_index_2 = [i for i in range(m2) if i != i2] + column_index_2 = [j for j in range(n2) if j != k1 and j != k2] + a2 = first_mat.matrix_from_rows_and_columns(row_index_1, [j1]) + b1 = second_mat.matrix_from_rows_and_columns(row_index_2, [k1]) + + a1 = first_mat.matrix_from_rows_and_columns([i1], column_index_1) + b2 = second_mat.matrix_from_rows_and_columns([i2], column_index_2) + + A = first_mat.matrix_from_rows_and_columns(row_index_1, column_index_1) + B = second_mat.matrix_from_rows_and_columns(row_index_2, column_index_2) + + first_subrows = A.rows() + second_subrows = B.rows() + upper_right_rows = a2.tensor_product(b2).rows() + lower_left_rows = b1.tensor_product(a1).rows() + + row_list = [] + for i in range(m1 - 1): + r = list(first_subrows[i]) + u = list(upper_right_rows[i]) + r.extend(u) + row_list.append(r) + for i in range(m2 - 1): + r = list(lower_left_rows[i]) + u = list(second_subrows[i]) + r.extend(u) + row_list.append(r) + M = Matrix_cmr_chr_sparse._from_data(row_list, immutable=False) + + if not verify: + return M + result = M.is_three_sum_wide_wide(first_mat, second_mat, + first_row_index=first_row_index, + first_columns_index=first_columns_index, + second_row_index=second_row_index, + second_columns_index=second_columns_index, + sign_verify=sign_verify) + if result is True: + return M + elif result is False: + raise ValueError('The given two matrices and related indices ' + 'do not satisfy the rule for three sum!') + else: + return result[1] + + def three_sum_mixed_mixed(first_mat, second_mat, + first_rows_index=[-2, -1], + first_column_index=-1, + second_row_index=0, + second_columns_index=[0, 1], + algorithm="cmr", + verify=True, + sign_verify=False): + r""" + Return the 3-sum matrix constructed from the given matrices ``first_mat`` and + ``second_mat``, assume that ``three_sum_strategy="concentrated_rank"``. + The first matrix is + `M_1=\begin{bmatrix} A & 0 \\ a_1^T & 1\\ a_2^T & 1\end{bmatrix}` + and the second matrix is + `M_2=\begin{bmatrix} 1 & 1 & 0\\ b_1 & b_2 & B\end{bmatrix}`. + Then the Truemper 3-sum is the matrix + `M_1 \oplus_3 M_2 = \begin{bmatrix} A & 0 \\ C & B\end{bmatrix}`, + where `\begin{bmatrix}a_1^T \\ a_2^T\end{bmatrix}=\begin{bmatrix} C_1 & \bar{C}\end{bmatrix}`, + `\begin{bmatrix}b_1 & b_2\end{bmatrix}=\begin{bmatrix} \bar{C}\\ C_2\end{bmatrix}`, + `C_12 = C_2 \bar{C}^{-1} C_1`, + `C=\begin{bmatrix} C_1 & \bar{C} \\ C_{12} & C_2\end{bmatrix}`, i.e., + `C=\begin{bmatrix}b_1 & b_2\end{bmatrix}\bar{C}^{-1}\begin{bmatrix}a_1^T\\ a_2^T\end{bmatrix}` + + The terminology "3-sum" is used in the context of Seymour's decomposition + of totally unimodular matrices and regular matroids, see [Sch1986]_, Ch. 19.4. + + .. SEEALSO:: :meth:`three_sum`, :meth:`is_three_sum`, + :meth:`three_sum_mixed_mixed` + + INPUT: + + - ``first_mat`` -- the first integer matrix `M_1` + - ``second_mat`` -- the second integer matrix `M_2` + - ``first_rows_index`` -- the indices of rows `a_1^T` and `a_2^T` in `M_1` + - ``first_column_index`` -- the index of the extra column in `M_1` + - ``second_row_index`` -- the index of the extra row in `M_2` + - ``second_columns_index`` -- the indices of columns `b_1` and `b_2` in `M_2` + - ``algorithm`` -- ``"cmr"`` or ``"direct"`` + If ``algorithm="cmr"``, then use :meth:`_three_sum_cmr`; + If ``algorithm="direct"``, then construct three sum directly. + - ``verify`` -- boolean (default: ``True``); + whether to check the give two matrices and the related indices + satisfying the requirements of 3-sum + by calling :meth:`is_three_sum_mixed_mixed`. + - ``sign_verify`` -- boolean (default: ``False``); + whether to check the sign correctness. + See :meth:`is_three_sum_mixed_mixed`. + + OUTPUT: A :class:`Matrix_cmr_chr_sparse` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M1 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 6, sparse=True), + ....: [[1, 1, 0, 0, 0, 0], + ....: [0, 0,-1, 1, 0, 0], + ....: [0, 1, 1, 0, 1, 0], + ....: [1, 0, 1,-1, 1, 1], + ....: [0,-1, 1, 0,-1, 1]]); M1 + [ 1 1 0 0 0 0] + [ 0 0 -1 1 0 0] + [ 0 1 1 0 1 0] + [ 1 0 1 -1 1 1] + [ 0 -1 1 0 -1 1] + sage: M2 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 6, 5, sparse=True), + ....: [[ 1, 1, 0, 0, 0], + ....: [ 1, 0, 1,-1, 0], + ....: [ 1,-1, 1, 1, 1], + ....: [-1, 1, 0, 0, 0], + ....: [ 0, 0, 1, 0,-1], + ....: [ 0, 1, 0, 1, 0]]); M2 + [ 1 1 0 0 0] + [ 1 0 1 -1 0] + [ 1 -1 1 1 1] + [-1 1 0 0 0] + [ 0 0 1 0 -1] + [ 0 1 0 1 0] + sage: Matrix_cmr_chr_sparse.three_sum_mixed_mixed(M1, M2) + [ 1 1 0 0 0 0 0 0] + [ 0 0 -1 1 0 0 0 0] + [ 0 1 1 0 1 0 0 0] + [ 1 0 1 -1 1 1 -1 0] + [ 1 1 0 -1 2 1 1 1] + [-1 -1 0 1 -2 0 0 0] + [ 0 0 0 0 0 1 0 -1] + [ 0 -1 1 0 -1 0 1 0] + + sage: M1 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 6, sparse=True), + ....: [[1, 1, 0, 0, 0, 0], + ....: [1, 0, 1,-1, 1, 1], + ....: [0,-1, 1, 0,-1, 1], + ....: [0, 0,-1, 1, 0, 0], + ....: [0, 1, 1, 0, 1, 0]]); M1 + [ 1 1 0 0 0 0] + [ 1 0 1 -1 1 1] + [ 0 -1 1 0 -1 1] + [ 0 0 -1 1 0 0] + [ 0 1 1 0 1 0] + sage: M2 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 6, 5, sparse=True), + ....: [[0, 0, 1, 0, 1], + ....: [1,-1, 1, 0, 0], + ....: [1, 1, 1, 1,-1], + ....: [0, 0,-1, 0, 1], + ....: [1, 0, 0,-1, 0], + ....: [0, 1, 0, 0, 1]]); M2 + [ 0 0 1 0 1] + [ 1 -1 1 0 0] + [ 1 1 1 1 -1] + [ 0 0 -1 0 1] + [ 1 0 0 -1 0] + [ 0 1 0 0 1] + sage: M = M1.three_sum_mixed_mixed(M2, first_rows_index=[1, 2], + ....: second_columns_index=[2, 4]); M + [ 1 1 0 0 0 0 0 0] + [ 0 0 -1 1 0 0 0 0] + [ 0 1 1 0 1 0 0 0] + [ 1 0 1 -1 1 1 -1 0] + [ 1 1 0 -1 2 1 1 1] + [-1 -1 0 1 -2 0 0 0] + [ 0 0 0 0 0 1 0 -1] + [ 0 -1 1 0 -1 0 1 0] + sage: N = M1.three_sum_mixed_mixed(M2, first_rows_index=[1, 2], + ....: second_columns_index=[2, 4], algorithm="direct") + sage: M == N + True + sage: M1.three_sum_mixed_mixed(M2, first_rows_index=[1, 2], + ....: first_column_index=1, + ....: second_columns_index=[2, 4]) + Traceback (most recent call last): + ... + ValueError: The given two matrices and related indices do not satisfy the rule for three sum! + sage: M1.three_sum_mixed_mixed(M2, first_rows_index=[1, 2], + ....: first_column_index=1, + ....: second_columns_index=[2, 4], verify=False) + [ 1 0 0 0 0 0 0 0] + [ 0 -1 1 0 0 0 0 0] + [ 0 1 0 1 0 0 0 0] + [ 1 1 -1 1 1 1 -1 0] + [ 1 0 -1 2 0 1 1 1] + [-1 0 1 -2 0 0 0 0] + [ 0 0 0 0 0 1 0 -1] + [ 0 1 0 -1 1 0 1 0] + """ + m1 = first_mat.nrows() + n1 = first_mat.ncols() + m2 = second_mat.nrows() + n2 = second_mat.ncols() + j1 = first_rows_index[0] + j2 = first_rows_index[1] + j1 = j1 if j1 >= 0 else m1 + j1 + j2 = j2 if j2 >= 0 else m1 + j2 + i1 = first_column_index + i1 = i1 if i1 >= 0 else n1 + i1 + k1 = second_columns_index[0] + k2 = second_columns_index[1] + k1 = k1 if k1 >= 0 else n2 + k1 + k2 = k2 if k2 >= 0 else n2 + k2 + i2 = second_row_index + i2 = i2 if i2 >= 0 else m2 + i2 + if j1 > j2: + j2, j1 = j1, j2 + if k1 > k2: + k2, k1 = k1, k2 + + if algorithm not in ["cmr", "direct"]: + raise ValueError("Unknown algorithm", algorithm) + + if algorithm == "cmr": + column_index_1 = [j for j in range(n1) if j != i1] + row_index_2 = [i for i in range(m2) if i != i2] + first_submat = first_mat.matrix_from_rows_and_columns(range(m1), column_index_1) + second_submat = second_mat.matrix_from_rows_and_columns(row_index_2, range(n2)) + + M = Matrix_cmr_chr_sparse._three_sum_cmr(first_submat, second_submat, + j1, j2, + k1, k2, + three_sum_strategy="concentrated_rank") + if algorithm == "direct": + row_index_1 = [i for i in range(m1) if i != j1 and i != j2] + column_index_1 = [j for j in range(n1) if j != i1] + row_index_2 = [i for i in range(m2) if i != i2] + column_index_2 = [j for j in range(n2) if j != k1 and j != k2] + a1 = first_mat.matrix_from_rows_and_columns([j1], column_index_1) + a2 = first_mat.matrix_from_rows_and_columns([j2], column_index_1) + b1 = second_mat.matrix_from_rows_and_columns(row_index_2, [k1]) + b2 = second_mat.matrix_from_rows_and_columns(row_index_2, [k2]) + + A = first_mat.matrix_from_rows_and_columns(row_index_1, column_index_1) + B = second_mat.matrix_from_rows_and_columns(row_index_2, column_index_2) + + first_subrows = A.rows() + second_subrows = B.rows() + lower_left_rows = (b1.tensor_product(a1) + b2.tensor_product(a2)).rows() + + row_list = [] + for i in range(m1 - 2): + r = list(first_subrows[i]) + u = [0 for j in range(n2 - 2)] + r.extend(u) + row_list.append(r) + for i in range(m2 - 1): + r = list(lower_left_rows[i]) + u = list(second_subrows[i]) + r.extend(u) + row_list.append(r) + M = Matrix_cmr_chr_sparse._from_data(row_list, immutable=False) + + if not verify: + return M + result = M.is_three_sum_mixed_mixed(first_mat, second_mat, + first_rows_index=first_rows_index, + first_column_index=first_column_index, + second_row_index=second_row_index, + second_columns_index=second_columns_index, + sign_verify=sign_verify) + if result is True: + return M + elif result is False: + raise ValueError('The given two matrices and related indices ' + 'do not satisfy the rule for three sum!') + else: + return result[1] + + def three_sum(first_mat, second_mat, + first_one_index=-1, + first_two_indices=[-2, -1], + second_one_index=0, + second_two_indices=[0, 1], + three_sum_strategy="distributed_ranks", + algorithm="cmr", + verify=True, + sign_verify=False): + r""" + Return the 3-sum matrix constructed from the given matrices + ``first_mat`` and ``second_mat``. + + .. SEEALSO:: :meth:`three_sum_wide_wide`, + :meth:`three_sum_mixed_mixed` + + INPUT: + + - ``first_mat`` -- the first integer matrix `M_1` + - ``second_mat`` -- the second integer matrix `M_2` + - ``first_one_index`` -- the index of one extra row/column in `M_1` + - ``first_two_indices`` -- the indices of two extra rows/columns in `M_1` + - ``second_one_index`` -- the index of one extra row/column in `M_2` + - ``second_two_indices`` -- the indices of two extra rows/columns in `M_2` + - ``three_sum_strategy`` -- ``"distributed_ranks"`` or ``"Wide_Wide"`` or + ``concentrated_rank`` or ``"Mixed_Mixed"`` + - ``algorithm`` -- ``"cmr"`` or ``"direct"`` + If ``algorithm="cmr"``, then use :meth:`_three_sum_cmr`; + If ``algorithm="direct"``, then construct three sum directly. + - ``verify`` -- boolean (default: ``True``); + whether to check the give two matrices and the related indices + satisfying the requirements of 3-sum + by calling :meth:`is_three_sum_wide_wide` or + :meth:`is_three_sum_mixed_mixed`. + - ``sign_verify`` -- boolean (default: ``False``); + whether to check the sign correctness. + See :meth:`is_three_sum`. + + OUTPUT: A :class:`Matrix_cmr_chr_sparse` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M1 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 6, sparse=True), + ....: [[1, 1, 0, 0, 0, 0], + ....: [0,-1, 0,-1, 1, 1], + ....: [0, 0, 1, 0,-1,-1], + ....: [0, 1, 0, 1, 1, 1], + ....: [1, 0,-1, 1, 0, 1],]); M1 + [ 1 1 0 0 0 0] + [ 0 -1 0 -1 1 1] + [ 0 0 1 0 -1 -1] + [ 0 1 0 1 1 1] + [ 1 0 -1 1 0 1] + sage: M2 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 6, sparse=True), + ....: [[ 1, 0, 1, 1, 1,-1], + ....: [-1,-1, 1, 1, 0, 0], + ....: [ 0, 0, 0,-1, 0, 1], + ....: [ 0, 0, 1, 0,-1, 0], + ....: [ 1, 1, 0, 0, 0, 1]]); M2 + [ 1 0 1 1 1 -1] + [-1 -1 1 1 0 0] + [ 0 0 0 -1 0 1] + [ 0 0 1 0 -1 0] + [ 1 1 0 0 0 1] + sage: M = Matrix_cmr_chr_sparse.three_sum(M1, M2); M + [ 1 1 0 0 0 0 0 0] + [ 0 -1 0 -1 1 1 1 -1] + [ 0 0 1 0 -1 -1 -1 1] + [ 0 1 0 1 1 1 1 -1] + [-1 0 1 -1 1 1 0 0] + [ 0 0 0 0 0 -1 0 1] + [ 0 0 0 0 1 0 -1 0] + [ 1 0 -1 1 0 0 0 1] + sage: M = Matrix_cmr_chr_sparse.three_sum(M1, M2) + + sage: M1 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 6, sparse=True), + ....: [[1, 1, 0, 0, 0, 0], + ....: [0, 0,-1, 1, 0, 0], + ....: [0, 1, 1, 0, 1, 0], + ....: [1, 0, 1,-1, 1, 1], + ....: [0,-1, 1, 0,-1, 1]]); M1 + [ 1 1 0 0 0 0] + [ 0 0 -1 1 0 0] + [ 0 1 1 0 1 0] + [ 1 0 1 -1 1 1] + [ 0 -1 1 0 -1 1] + sage: M2 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 6, 5, sparse=True), + ....: [[ 1, 1, 0, 0, 0], + ....: [ 1, 0, 1,-1, 0], + ....: [ 1,-1, 1, 1, 1], + ....: [-1, 1, 0, 0, 0], + ....: [ 0, 0, 1, 0,-1], + ....: [ 0, 1, 0, 1, 0]]); M2 + [ 1 1 0 0 0] + [ 1 0 1 -1 0] + [ 1 -1 1 1 1] + [-1 1 0 0 0] + [ 0 0 1 0 -1] + [ 0 1 0 1 0] + sage: M = Matrix_cmr_chr_sparse.three_sum(M1, M2, three_sum_strategy="Mixed_Mixed"); M + [ 1 1 0 0 0 0 0 0] + [ 0 0 -1 1 0 0 0 0] + [ 0 1 1 0 1 0 0 0] + [ 1 0 1 -1 1 1 -1 0] + [ 1 1 0 -1 2 1 1 1] + [-1 -1 0 1 -2 0 0 0] + [ 0 0 0 0 0 1 0 -1] + [ 0 -1 1 0 -1 0 1 0] + + sage: M1.three_sum(M2, three_sum_strategy="Wide_Mixed") + Traceback (most recent call last): + ... + ValueError: ('Unknown three sum mode', 'Wide_Mixed') + + sage: M1 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 6, sparse=True), + ....: [[1, 1, 0, 0, 0, 0], + ....: [1, 0, 1,-1, 1, 1], + ....: [0,-1, 1, 0,-1, 1], + ....: [0, 0,-1, 1, 0, 0], + ....: [0, 1, 1, 0, 1, 0]]); M1 + [ 1 1 0 0 0 0] + [ 1 0 1 -1 1 1] + [ 0 -1 1 0 -1 1] + [ 0 0 -1 1 0 0] + [ 0 1 1 0 1 0] + sage: M2 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 6, 5, sparse=True), + ....: [[0, 0, 1, 0, 1], + ....: [1,-1, 1, 0, 0], + ....: [1, 1, 1, 1,-1], + ....: [0, 0,-1, 0, 1], + ....: [1, 0, 0,-1, 0], + ....: [0, 1, 0, 0, 1]]); M2 + [ 0 0 1 0 1] + [ 1 -1 1 0 0] + [ 1 1 1 1 -1] + [ 0 0 -1 0 1] + [ 1 0 0 -1 0] + [ 0 1 0 0 1] + sage: M1.three_sum(M2, first_two_indices=[1, 2], + ....: second_two_indices=[2, 4], + ....: three_sum_strategy="concentrated_rank") + [ 1 1 0 0 0 0 0 0] + [ 0 0 -1 1 0 0 0 0] + [ 0 1 1 0 1 0 0 0] + [ 1 0 1 -1 1 1 -1 0] + [ 1 1 0 -1 2 1 1 1] + [-1 -1 0 1 -2 0 0 0] + [ 0 0 0 0 0 1 0 -1] + [ 0 -1 1 0 -1 0 1 0] + + sage: M1 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 6, sparse=True), + ....: [[1, 1, 0, 0, 0, 0], + ....: [1, 0, 1,-1, 1, 0], + ....: [0,-1, 1, 0,-1, 1], + ....: [0, 0,-1, 1, 0,-1], + ....: [0, 1, 1, 0, 1, 1]]); M1 + [ 1 1 0 0 0 0] + [ 1 0 1 -1 1 0] + [ 0 -1 1 0 -1 1] + [ 0 0 -1 1 0 -1] + [ 0 1 1 0 1 1] + sage: M2 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 6, sparse=True), + ....: [[1,-1, 1, 0, 0,-1], + ....: [1, 1, 1, 1,-1, 0], + ....: [0, 0,-1, 0, 1, 0], + ....: [1, 0, 0,-1, 0, 0], + ....: [0, 1, 0, 0, 1, 1]]); M2 + [ 1 -1 1 0 0 -1] + [ 1 1 1 1 -1 0] + [ 0 0 -1 0 1 0] + [ 1 0 0 -1 0 0] + [ 0 1 0 0 1 1] + sage: M1.three_sum(M2, 1, [2,-1], 1, [1, -1]) + [ 1 1 0 0 0 0 0 0] + [ 0 -1 0 -1 1 1 1 -1] + [ 0 0 1 0 -1 -1 -1 1] + [ 0 1 0 1 1 1 1 -1] + [-1 0 1 -1 1 1 0 0] + [ 0 0 0 0 0 -1 0 1] + [ 0 0 0 0 1 0 -1 0] + [ 1 0 -1 1 0 0 0 1] + """ + if three_sum_strategy in ["distributed_ranks", "Wide_Wide"]: + return first_mat.three_sum_wide_wide(second_mat, + first_row_index=first_one_index, + first_columns_index=first_two_indices, + second_row_index=second_one_index, + second_columns_index=second_two_indices, + algorithm=algorithm, + verify=verify, + sign_verify=sign_verify) + if three_sum_strategy in ["concentrated_rank", "Mixed_Mixed"]: + return first_mat.three_sum_mixed_mixed(second_mat, + first_rows_index=first_two_indices, + first_column_index=first_one_index, + second_row_index=second_one_index, + second_columns_index=second_two_indices, + algorithm=algorithm, + verify=verify, + sign_verify=sign_verify) + raise ValueError("Unknown three sum mode", three_sum_strategy) + + def is_three_sum_wide_wide(three_sum_mat, first_mat, second_mat, + first_row_index=-1, + first_columns_index=[-2, -1], + second_row_index=0, + second_columns_index=[0, 1], + sign_verify=True): + r""" + Check whether ``first_mat`` and ``second_mat`` form ``three_sum_mat`` + via the 3-sum operation. + Assume that ``three_sum_strategy="distributed_ranks"`` or ``"Wide_Wide"``. + If ``sign_verify=True``, also check whether the 3-sum satisfies that + ``three_sum_mat`` is totally unimodular, if and only if, + ``first_mat`` and ``second_mat`` are both totally unimodular. + + The first matrix is + `M_1=\begin{bmatrix} A & a_2 & a_2\\ a_1^T & 0 & \epsilon_2\end{bmatrix}` + and the second matrix is + `M_2=\begin{bmatrix} \epsilon_1 & 0 & b_2^T\\ b_1 & b_1 & B\end{bmatrix}`, + where `\epsilon_1`, `\epsilon_2` are `1` or `-1`. + Then the Seymour/Schrijver 3-sum is the matrix + `M_1 \oplus_3 M_2 = \begin{bmatrix} A & a_2 b_2^T \\ b_1 a_1^T & B\end{bmatrix}`. + + The terminology "3-sum" is used in the context of Seymour's decomposition + of totally unimodular matrices and regular matroids, see [Sch1986]_, Ch. 19.4. + + The signs of `\epsilon_1` (`\epsilon_2`) are determined by + a shortest path between two sets of vertices in the bipartite graph, + where the sets of vertices corresponding to the nonzero + row and column indices of `a_1^T, a_2` (`b_2^T, b_1`), + and the bipartite graph consists of vertices corresponding to the rows + and columns of `M`, and edges corresponding to the nonzero entry. + between the rows and columns of `M`, see [Sch1986]_, Ch. 20.3. + + .. SEEALSO:: :meth:`three_sum_wide_wide`, :meth:`is_three_sum_mixed_mixed` + :meth:`is_totally_unimodular` + + INPUT: + + - ``three_sum_mat`` -- the large integer matrix `M` + - ``first_mat`` -- the first integer matrix `M_1` + - ``second_mat`` -- the second integer matrix `M_2` + - ``first_row_index`` -- the row index of `a_1^T` in `M_1` + - ``first_columns_index`` -- the column indices of `a_2` in `M_1` + - ``second_row_index`` -- the row index of `b_2^T` in `M_2` + - ``second_columns_index`` -- the column indices of `b_1` in `M_2` + - ``sign_verify`` -- boolean (default: ``True``); + whether to check the sign correctness of `\epsilon_1` and `\epsilon_2`. + + OUTPUT: boolean, or (boolean, string) + + If it is False only because of the sign, then also output the correct sign. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M1 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 6, sparse=True), + ....: [[1, 1, 0, 0, 0, 0], + ....: [0,-1, 0,-1, 1, 1], + ....: [0, 0, 1, 0,-1,-1], + ....: [0, 1, 0, 1, 1, 1], + ....: [1, 0,-1, 1, 0, 1],]); M1 + [ 1 1 0 0 0 0] + [ 0 -1 0 -1 1 1] + [ 0 0 1 0 -1 -1] + [ 0 1 0 1 1 1] + [ 1 0 -1 1 0 1] + sage: M2 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 6, sparse=True), + ....: [[ 1, 0, 1, 1, 1,-1], + ....: [-1,-1, 1, 1, 0, 0], + ....: [ 0, 0, 0,-1, 0, 1], + ....: [ 0, 0, 1, 0,-1, 0], + ....: [ 1, 1, 0, 0, 0, 1]]); M2 + [ 1 0 1 1 1 -1] + [-1 -1 1 1 0 0] + [ 0 0 0 -1 0 1] + [ 0 0 1 0 -1 0] + [ 1 1 0 0 0 1] + sage: M = Matrix_cmr_chr_sparse.three_sum_wide_wide(M1, M2); M + [ 1 1 0 0 0 0 0 0] + [ 0 -1 0 -1 1 1 1 -1] + [ 0 0 1 0 -1 -1 -1 1] + [ 0 1 0 1 1 1 1 -1] + [-1 0 1 -1 1 1 0 0] + [ 0 0 0 0 0 -1 0 1] + [ 0 0 0 0 1 0 -1 0] + [ 1 0 -1 1 0 0 0 1] + sage: M.is_three_sum_wide_wide(M1, M2, sign_verify=False) + True + sage: M.is_three_sum_wide_wide(M1, M2) + (False, + 'sign_1 in second_mat should be -1. sign_2 in first_mat should be -1. ') + + sage: M1 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 4, 5, sparse=True), + ....: [[ 0, 0, 1,-1,-1], + ....: [ 1, 1, 1, 0, 0], + ....: [ 0, 1, 0, 1, 1], + ....: [-1, 0,-1, 0, 1]]); M1 + [ 0 0 1 -1 -1] + [ 1 1 1 0 0] + [ 0 1 0 1 1] + [-1 0 -1 0 1] + sage: M2 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 4, 5, sparse=True), + ....: [[ 1, 0, 1,-1, 0], + ....: [ 0, 0, 1, 0, 1], + ....: [-1,-1, 0, 1, 1], + ....: [-1,-1, 0, 0, 1]]); M2 + [ 1 0 1 -1 0] + [ 0 0 1 0 1] + [-1 -1 0 1 1] + [-1 -1 0 0 1] + sage: M = Matrix_cmr_chr_sparse.three_sum_wide_wide(M1, M2); M + [ 0 0 1 -1 1 0] + [ 1 1 1 0 0 0] + [ 0 1 0 1 -1 0] + [ 0 0 0 1 0 1] + [ 1 0 1 0 1 1] + [ 1 0 1 0 0 1] + sage: Matrix_cmr_chr_sparse.is_three_sum_wide_wide(M, M1, M2) + True + """ + if not isinstance(first_columns_index, (list, tuple)) or len(first_columns_index) != 2: + raise ValueError('The index of two columns needs to be given!') + if not isinstance(second_columns_index, (list, tuple)) or len(second_columns_index) != 2: + raise ValueError('The index of two columns needs to be given!') + + m1 = first_mat.nrows() + n1 = first_mat.ncols() + m2 = second_mat.nrows() + n2 = second_mat.ncols() + m = three_sum_mat.nrows() + n = three_sum_mat.ncols() + if m != (m1 + m2 - 2): # The number of rows should match + return False + if n != (n1 + n2 - 4): # The number of columns should match + return False + + # Check the extra two columns for a2 and b1 + j1 = first_columns_index[0] + j2 = first_columns_index[1] + j1 = j1 if j1 >= 0 else n1 + j1 + j2 = j2 if j2 >= 0 else n1 + j2 + i1 = first_row_index + i1 = i1 if i1 >= 0 else m1 + i1 + row_index_1 = [i for i in range(m1) if i != i1] + for i in row_index_1: + if first_mat[i, j1] != first_mat[i, j2]: + return False + sign_2 = first_mat[i1, j2] if first_mat[i1, j1] == 0 else first_mat[i1, j1] + if sign_2 == 0: + return False + + k1 = second_columns_index[0] + k2 = second_columns_index[1] + k1 = k1 if k1 >= 0 else n2 + k1 + k2 = k2 if k2 >= 0 else n2 + k2 + i2 = second_row_index + i2 = i2 if i2 >= 0 else m2 + i2 + row_index_2 = [i for i in range(m2) if i != i2] + for i in row_index_2: + if second_mat[i, k1] != second_mat[i, k2]: + return False + sign_1 = second_mat[i2, k2] if second_mat[i2, k1] == 0 else second_mat[i2, k1] + if sign_1 == 0: + return False + + # Check whether the result comes from the three sum + column_index_1 = [j for j in range(n1) if j != j1 and j != j2] + for i in range(m1 - 1): + for j in range(n1 - 2): + if first_mat[row_index_1[i], column_index_1[j]] != three_sum_mat[i, j]: + return False + column_index_2 = [j for j in range(n2) if j != k1 and j != k2] + for i in range(m2 - 1): + for j in range(n2 - 2): + if second_mat[row_index_2[i], column_index_2[j]] != three_sum_mat[m1 - 1 + i, n1 - 2 + j]: + return False + for i in range(m1 - 1): + for j in range(n2 - 2): + rank1_entry = first_mat[row_index_1[i], j1] * second_mat[i2, column_index_2[j]] + if rank1_entry != three_sum_mat[i, n1 - 2 + j]: + return False + for i in range(m2 - 1): + for j in range(n1 - 2): + rank1_entry = first_mat[i1, column_index_1[j]] * second_mat[row_index_2[i], k1] + if rank1_entry != three_sum_mat[m1 - 1 + i, j]: + return False + + if sign_verify is not True: + return True + # Check the sign + + from sage.graphs.graph import Graph + G = Graph() + + rows = ['r' + str(i) for i in range(m)] + cols = ['c' + str(j) for j in range(n)] + G.add_vertices(rows + cols) + + for i in range(m): + for j in range(n): + if three_sum_mat[i, j] != 0: + G.add_edge('r' + str(i), 'c' + str(j)) + dist_dict = G.distance_all_pairs() + + R1 = ['r'+str(i) for i in range(m1 - 1) if first_mat[row_index_1[i], j1] != 0] + K1 = ['c'+str(j) for j in range(n1 - 2) if first_mat[i1, column_index_1[j]] != 0] + + min_distance = float('inf') + min_pair = None + for v1 in R1: + for v2 in K1: + if v2 in dist_dict[v1]: + if dist_dict[v1][v2] < min_distance: + min_distance = dist_dict[v1][v2] + min_pair = (v1, v2) + path_1 = G.shortest_path(min_pair[0], min_pair[1]) + path_1_num = [int(v[1:]) for v in path_1] + path_1_len = 0 + for i in range(len(path_1) - 1): + if path_1[i][0] == 'r': + r = path_1_num[i] + c = path_1_num[i + 1] + if path_1[i][0] == 'c': + c = path_1_num[i] + r = path_1_num[i + 1] + path_1_len += three_sum_mat[r, c] * first_mat[row_index_1[r], j1] * first_mat[i1, column_index_1[c]] + + R2 = ['r'+str(m1 - 1 + i) for i in range(m2 - 1) if second_mat[row_index_2[i], k1] != 0] + K2 = ['c'+str(n1 - 2 + j) for j in range(n2 - 2) if second_mat[i2, column_index_2[j]] != 0] + + min_distance = float('inf') + min_pair = None + for v1 in R2: + for v2 in K2: + if v2 in dist_dict[v1]: + if dist_dict[v1][v2] < min_distance: + min_distance = dist_dict[v1][v2] + min_pair = (v1, v2) + path_2 = G.shortest_path(min_pair[0], min_pair[1]) + path_2_num = [int(v[1:]) for v in path_2] + path_2_len = 0 + for i in range(len(path_2) - 1): + if path_2[i][0] == 'r': + r = path_2_num[i] + c = path_2_num[i + 1] + if path_2[i][0] == 'c': + c = path_2_num[i] + r = path_2_num[i + 1] + path_2_len += three_sum_mat[r, c] * second_mat[row_index_2[r - m1 + 1], k1] * second_mat[i2, column_index_2[c - n1 + 2]] + + msg = "" + if (sign_1 - path_1_len) % 4 != 0: + msg += f'sign_1 in second_mat should be {-sign_1}. ' + if (sign_2 - path_2_len) % 4 != 0: + msg += f'sign_2 in first_mat should be {-sign_2}. ' + if msg: + return False, msg + return True + + def is_three_sum_mixed_mixed(three_sum_mat, first_mat, second_mat, + first_rows_index=[-2, -1], + first_column_index=-1, + second_row_index=0, + second_columns_index=[0, 1], + sign_verify=True): + r""" + Check whether ``first_mat`` and ``second_mat`` form ``three_sum_mat`` + via the 3-sum operation. + Assume that ``three_sum_strategy="concentrated_ranks"`` or ``"Mixed_Mixed"``. + If ``sign_verify=True``, also check whether the 3-sum satisfies that + ``three_sum_mat`` is totally unimodular, if and only if, + ``first_mat`` and ``second_mat`` are both totally unimodular. + + The first matrix is + `M_1=\begin{bmatrix} A & 0 \\ a_1^T & 1\\ a_2^T & \epsilon_2\end{bmatrix}` + and the second matrix is + `M_2=\begin{bmatrix} \epsilon_1 & 1 & 0\\ b_1 & b_2 & B\end{bmatrix}`, + where `\epsilon_1`, `\epsilon_2` are `1` or `-1`. + Then the Truemper 3-sum is the matrix + `M_1 \oplus_3 M_2 = \begin{bmatrix} A & 0 \\ C & B\end{bmatrix}`, + where `\begin{bmatrix}a_1^T \\ a_2^T\end{bmatrix}=\begin{bmatrix} C_1 & \bar{C}\end{bmatrix}`, + `\begin{bmatrix}b_1 & b_2\end{bmatrix}=\begin{bmatrix} \bar{C}\\ C_2\end{bmatrix}`, + `C_12 = C_2 \bar{C}^{-1} C_1`, + `C=\begin{bmatrix} C_1 & \bar{C} \\ C_{12} & C_2\end{bmatrix}`, i.e., + `C=\begin{bmatrix}b_1 & b_2\end{bmatrix}\bar{C}^{-1}\begin{bmatrix}a_1^T\\ a_2^T\end{bmatrix}` + + The terminology "3-sum" is used in the context of Seymour's decomposition + of totally unimodular matrices and regular matroids, see [Sch1986]_, Ch. 19.4. + + .. SEEALSO:: :meth:`is_three_sum_wide_wide`, :meth:`three_sum_mixed_mixed` + :meth:`is_totally_unimodular` + + INPUT: + + - ``three_sum_mat`` -- the large integer matrix `M` + - ``first_mat`` -- the first integer matrix `M_1` + - ``second_mat`` -- the second integer matrix `M_2` + - ``first_rows_index`` -- the indices of rows `a_1^T` and `a_2^T` in `M_1` + - ``first_column_index`` -- the index of the column with `\epsilon_2` in `M_1` + - ``second_row_index`` -- the index of the row with `\epsilon_1` in `M_2` + - ``second_columns_index`` -- the indices of columns `b_1` and `b_2` in `M_2` + - ``sign_verify`` -- boolean (default: ``True``); + whether to check the sign correctness of `\epsilon_1` and `\epsilon_2`. + + OUTPUT: boolean, or (boolean, string) + + If it is False only because of the sign, then also output the correct sign. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M1 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 6, sparse=True), + ....: [[1, 1, 0, 0, 0, 0], + ....: [0, 0,-1, 1, 0, 0], + ....: [0, 1, 1, 0, 1, 0], + ....: [1, 0, 1,-1, 1, 1], + ....: [0,-1, 1, 0,-1, 1]]); M1 + [ 1 1 0 0 0 0] + [ 0 0 -1 1 0 0] + [ 0 1 1 0 1 0] + [ 1 0 1 -1 1 1] + [ 0 -1 1 0 -1 1] + sage: M2 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 6, 5, sparse=True), + ....: [[ 1, 1, 0, 0, 0], + ....: [ 1, 0, 1,-1, 0], + ....: [ 1,-1, 1, 1, 1], + ....: [-1, 1, 0, 0, 0], + ....: [ 0, 0, 1, 0,-1], + ....: [ 0, 1, 0, 1, 0]]); M2 + [ 1 1 0 0 0] + [ 1 0 1 -1 0] + [ 1 -1 1 1 1] + [-1 1 0 0 0] + [ 0 0 1 0 -1] + [ 0 1 0 1 0] + sage: M = Matrix_cmr_chr_sparse.three_sum_mixed_mixed(M1, M2); M + [ 1 1 0 0 0 0 0 0] + [ 0 0 -1 1 0 0 0 0] + [ 0 1 1 0 1 0 0 0] + [ 1 0 1 -1 1 1 -1 0] + [ 1 1 0 -1 2 1 1 1] + [-1 -1 0 1 -2 0 0 0] + [ 0 0 0 0 0 1 0 -1] + [ 0 -1 1 0 -1 0 1 0] + sage: M.is_three_sum_mixed_mixed(M1, M2, sign_verify=False) + True + sage: M.is_three_sum_mixed_mixed(M1, M2) + True + + sage: M1 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 4, 5, sparse=True), + ....: [[ 1, 0, 1, 1, 0], + ....: [ 0, 1, 1, 1, 0], + ....: [ 1, 0, 1, 0, 1], + ....: [ 0,-1, 0,-1, 1]]); M1 + [ 1 0 1 1 0] + [ 0 1 1 1 0] + [ 1 0 1 0 1] + [ 0 -1 0 -1 1] + sage: M2 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 4, sparse=True), + ....: [[ 1, 1, 0, 0], + ....: [ 1, 0, 1, 1], + ....: [ 0,-1, 1, 1], + ....: [ 1, 0, 1, 0], + ....: [ 0,-1, 0, 1]]); M2 + [ 1 1 0 0] + [ 1 0 1 1] + [ 0 -1 1 1] + [ 1 0 1 0] + [ 0 -1 0 1] + sage: M = Matrix_cmr_chr_sparse.three_sum_mixed_mixed(M1, M2); M + [1 0 1 1 0 0] + [0 1 1 1 0 0] + [1 0 1 0 1 1] + [0 1 0 1 1 1] + [1 0 1 0 1 0] + [0 1 0 1 0 1] + sage: Matrix_cmr_chr_sparse.is_three_sum_mixed_mixed(M, M1, M2) + True + """ + if not isinstance(first_rows_index, (list, tuple)) or len(first_rows_index) != 2: + raise ValueError('The index of two columns needs to be given!') + if not isinstance(second_columns_index, (list, tuple)) or len(second_columns_index) != 2: + raise ValueError('The index of two columns needs to be given!') + + m1 = first_mat.nrows() + n1 = first_mat.ncols() + m2 = second_mat.nrows() + n2 = second_mat.ncols() + m = three_sum_mat.nrows() + n = three_sum_mat.ncols() + if m != (m1 + m2 - 3): # The number of rows should match + return False + if n != (n1 + n2 - 3): # The number of columns should match + return False + + # Check whether the extra column of M1 is zero except the two extra rows + j1 = first_rows_index[0] + j2 = first_rows_index[1] + j1 = j1 if j1 >= 0 else m1 + j1 + j2 = j2 if j2 >= 0 else m1 + j2 + i1 = first_column_index + i1 = i1 if i1 >= 0 else n1 + i1 + row_index_1 = [i for i in range(m1) if i != j1 and i != j2] + for i in row_index_1: + if first_mat[i, i1] != 0: + return False + if first_mat[j1, i1] == 0 or first_mat[j2, i1] == 0: + return False + # Check whether the extra row of M2 is zero except the two extra columns + k1 = second_columns_index[0] + k2 = second_columns_index[1] + k1 = k1 if k1 >= 0 else n2 + k1 + k2 = k2 if k2 >= 0 else n2 + k2 + i2 = second_row_index + i2 = i2 if i2 >= 0 else m2 + i2 + column_index_2 = [j for j in range(n2) if j != k1 and j != k2] + for j in column_index_2: + if second_mat[i2, j] != 0: + return False + if second_mat[i2, k1] == 0 or second_mat[i2, k2] == 0: + return False + + # Check whether the result comes from the three sum + column_index_1 = [j for j in range(n1) if j != i1] + for i in range(m1 - 2): + for j in range(n1 - 1): + if first_mat[row_index_1[i], column_index_1[j]] != three_sum_mat[i, j]: + return False + row_index_2 = [i for i in range(m2) if i != i2] + for i in range(m2 - 1): + for j in range(n2 - 2): + if second_mat[row_index_2[i], column_index_2[j]] != three_sum_mat[m1 - 2 + i, n1 - 1 + j]: + return False + for i in range(m1 - 2): + for j in range(n2 - 2): + if three_sum_mat[i, n1 - 1 + j] != 0: + return False + for i in range(m2 - 1): + for j in range(n1 - 1): + rank2_entry = first_mat[j1, column_index_1[j]] * second_mat[row_index_2[i], k1] + first_mat[j2, column_index_1[j]] * second_mat[row_index_2[i], k2] + if rank2_entry != three_sum_mat[m1 - 2 + i, j]: + return False + + if sign_verify is not True: + return True + # Check the sign + sign_2 = first_mat[j1, i1] * first_mat[j2, i1] + sign_1 = second_mat[i2, k1] * second_mat[i2, k2] + + from sage.graphs.graph import Graph + G = Graph() + + rows = ['r' + str(i) for i in range(m)] + cols = ['c' + str(j) for j in range(n)] + G.add_vertices(rows + cols) + + for i in range(m): + for j in range(n): + if three_sum_mat[i, j] != 0: + G.add_edge('r' + str(i), 'c' + str(j)) + dist_dict = G.distance_all_pairs() + + K1 = [] + K2 = [] + b1 = second_mat.matrix_from_rows_and_columns(row_index_2, [k1]) + b2 = second_mat.matrix_from_rows_and_columns(row_index_2, [k2]) + for j in range(n1 - 1): + bb = three_sum_mat.matrix_from_rows_and_columns(range(m1 - 2, m), [j]) + if bb == b1 or bb == -b1: + K1.append('c'+str(j)) + elif bb == b2 or bb == -b2: + K2.append('c'+str(j)) + + min_distance = float('inf') + min_pair = None + for v1 in K1: + for v2 in K2: + if v2 in dist_dict[v1]: + if dist_dict[v1][v2] < min_distance: + min_distance = dist_dict[v1][v2] + min_pair = (v1, v2) + path_1 = G.shortest_path(min_pair[0], min_pair[1]) + path_1_num = [int(v[1:]) for v in path_1] + q = (len(path_1) + 1)/2 + path_1_len = (-1)**q + for i in range(q - 1): + path_1_len *= three_sum_mat[path_1_num[2*i + 1], path_1_num[2*i]] + path_1_len *= three_sum_mat[path_1_num[2*i + 1], path_1_num[2*i + 2]] + + R1 = [] + R2 = [] + a1 = first_mat.matrix_from_rows_and_columns([j1], column_index_1) + a2 = first_mat.matrix_from_rows_and_columns([j2], column_index_1) + for i in range(m2 - 1): + aa = three_sum_mat.matrix_from_rows_and_columns([m1 - 2 + i], range(n1 - 1)) + if aa == a1 or aa == -a1: + R1.append('r'+str(i)) + elif aa == a2 or aa == -a2: + R2.append('r'+str(i)) + + min_distance = float('inf') + min_pair = None + for v1 in R1: + for v2 in R2: + if v2 in dist_dict[v1]: + if dist_dict[v1][v2] < min_distance: + min_distance = dist_dict[v1][v2] + min_pair = (v1, v2) + path_2 = G.shortest_path(min_pair[0], min_pair[1]) + path_2_num = [int(v[1:]) for v in path_2] + p = (len(path_2) + 1)/2 + path_2_len = (-1)**p + for i in range(p - 1): + path_2_len *= three_sum_mat[path_2_num[2*i], path_2_num[2*i + 1]] + path_2_len *= three_sum_mat[path_2_num[2*i + 2], path_2_num[2*i + 1]] + + msg = "" + if (sign_1 - path_1_len) % 4 != 0: + msg += f'sign_1 in second_mat should be {-sign_1}. ' + if (sign_2 - path_2_len) % 4 != 0: + msg += f'sign_2 in first_mat should be {-sign_2}. ' + if msg: + return False, msg + return True + + def is_three_sum(three_sum_mat, first_mat, second_mat, + three_sum_strategy="distributed_ranks", + first_one_index=-1, + first_two_indices=[-2, -1], + second_one_index=0, + second_two_indices=[0, 1], + sign_verify=True): + r""" + Check whether ``first_mat`` and ``second_mat`` form ``three_sum_mat`` + via the 3-sum operation. + If ``sign_verify=True``, also check whether the 3-sum satisfies that + ``three_sum_mat`` is totally unimodular, if and only if, + ``first_mat`` and ``second_mat`` are both totally unimodular. + + .. SEEALSO:: :meth:`is_three_sum_wide_wide`, + :meth:`is_three_sum_mixed_mixed` + + INPUT: + + - ``three_sum_mat`` -- the large integer matrix `M` + - ``first_mat`` -- the first integer matrix `M_1` + - ``second_mat`` -- the second integer matrix `M_2` + - ``three_sum_strategy`` -- ``"distributed_ranks"`` or ``"Wide_Wide"`` or + ``concentrated_rank`` or ``"Mixed_Mixed"`` + - ``first_one_index`` -- the index of one extra row/column in `M_1` + - ``first_two_indices`` -- the indices of two extra rows/columns in `M_1` + - ``second_one_index`` -- the index of one extra row/column in `M_2` + - ``second_two_indices`` -- the indices of two extra rows/columns in `M_2` + - ``sign_verify`` -- boolean (default: ``True``); + whether to check the sign correctness. + + OUTPUT: boolean, or (boolean, string) + + If it is False only because of the sign, then also output the correct sign. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M1 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 6, sparse=True), + ....: [[1, 1, 0, 0, 0, 0], + ....: [0,-1, 0,-1, 1, 1], + ....: [0, 0, 1, 0,-1,-1], + ....: [0, 1, 0, 1, 1, 1], + ....: [1, 0,-1, 1, 0, 1],]); M1 + [ 1 1 0 0 0 0] + [ 0 -1 0 -1 1 1] + [ 0 0 1 0 -1 -1] + [ 0 1 0 1 1 1] + [ 1 0 -1 1 0 1] + sage: M2 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 6, sparse=True), + ....: [[ 1, 0, 1, 1, 1,-1], + ....: [-1,-1, 1, 1, 0, 0], + ....: [ 0, 0, 0,-1, 0, 1], + ....: [ 0, 0, 1, 0,-1, 0], + ....: [ 1, 1, 0, 0, 0, 1]]); M2 + [ 1 0 1 1 1 -1] + [-1 -1 1 1 0 0] + [ 0 0 0 -1 0 1] + [ 0 0 1 0 -1 0] + [ 1 1 0 0 0 1] + sage: M = Matrix_cmr_chr_sparse.three_sum_wide_wide(M1, M2); M + [ 1 1 0 0 0 0 0 0] + [ 0 -1 0 -1 1 1 1 -1] + [ 0 0 1 0 -1 -1 -1 1] + [ 0 1 0 1 1 1 1 -1] + [-1 0 1 -1 1 1 0 0] + [ 0 0 0 0 0 -1 0 1] + [ 0 0 0 0 1 0 -1 0] + [ 1 0 -1 1 0 0 0 1] + sage: M.is_three_sum(M1, M2, sign_verify=False) + True + sage: M.is_three_sum(M1, M2) + (False, + 'sign_1 in second_mat should be -1. sign_2 in first_mat should be -1. ') + + sage: M1 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 6, sparse=True), + ....: [[1, 1, 0, 0, 0, 0], + ....: [0, 0,-1, 1, 0, 0], + ....: [0, 1, 1, 0, 1, 0], + ....: [1, 0, 1,-1, 1, 1], + ....: [0,-1, 1, 0,-1, 1]]); M1 + [ 1 1 0 0 0 0] + [ 0 0 -1 1 0 0] + [ 0 1 1 0 1 0] + [ 1 0 1 -1 1 1] + [ 0 -1 1 0 -1 1] + sage: M2 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 6, 5, sparse=True), + ....: [[ 1, 1, 0, 0, 0], + ....: [ 1, 0, 1,-1, 0], + ....: [ 1,-1, 1, 1, 1], + ....: [-1, 1, 0, 0, 0], + ....: [ 0, 0, 1, 0,-1], + ....: [ 0, 1, 0, 1, 0]]); M2 + [ 1 1 0 0 0] + [ 1 0 1 -1 0] + [ 1 -1 1 1 1] + [-1 1 0 0 0] + [ 0 0 1 0 -1] + [ 0 1 0 1 0] + sage: M = Matrix_cmr_chr_sparse.three_sum_mixed_mixed(M1, M2); M + [ 1 1 0 0 0 0 0 0] + [ 0 0 -1 1 0 0 0 0] + [ 0 1 1 0 1 0 0 0] + [ 1 0 1 -1 1 1 -1 0] + [ 1 1 0 -1 2 1 1 1] + [-1 -1 0 1 -2 0 0 0] + [ 0 0 0 0 0 1 0 -1] + [ 0 -1 1 0 -1 0 1 0] + sage: M.is_three_sum(M1, M2, three_sum_strategy="Mixed_Mixed", sign_verify=False) + True + sage: M.is_three_sum(M1, M2, three_sum_strategy="Mixed_Mixed") + True + + sage: M.is_three_sum(M1, M2, three_sum_strategy="Wide_Mixed") + Traceback (most recent call last): + ... + ValueError: ('Unknown three sum mode', 'Wide_Mixed') + """ + if three_sum_strategy in ["distributed_ranks", "Wide_Wide"]: + return three_sum_mat.is_three_sum_wide_wide(first_mat, second_mat, + first_row_index=first_one_index, + first_columns_index=first_two_indices, + second_row_index=second_one_index, + second_columns_index=second_two_indices, + sign_verify=sign_verify) + if three_sum_strategy in ["concentrated_rank", "Mixed_Mixed"]: + return three_sum_mat.is_three_sum_mixed_mixed(first_mat, second_mat, + first_rows_index=first_two_indices, + first_column_index=first_one_index, + second_row_index=second_one_index, + second_columns_index=second_two_indices, + sign_verify=sign_verify) + raise ValueError("Unknown three sum mode", three_sum_strategy) + + def _three_sum(first_mat, second_mat, first_col_index1, first_col_index2, second_col_index1, second_col_index2): + r""" + Return the 3-sum matrix constructed from the given matrices ``first_mat`` and + ``second_mat``, with ``first_col_index1`` and ``first_col_index2`` being the + indices of the column vectors of the matrix, which are identical except for + one row having a 0 in one column and the other a non-zero entry in that row. + The method assumes the nonzero entry is one. The same goes are made for ``second_mat``, ``second_col_index1``, and ``second_col_index2``. + + The operation is defined in [Sch1986]_, Ch. 19.4.:= + [first_mat first_col first_col] ___|___ [second_mat second_col second_col] + [first_row 0 1 ] | 3 [second_row 0 1 ] + ----- [ first_mat first_col x second_row] + ----- [second_col x first_row second_col ] + + INPUT: + + - ``first_mat`` -- integer matrix having two collumns which are identical in + every entry except for one row in which one is 0 and the other is 1 + - ``second_mat`` -- integer matrix having two collumns which are identical in + every entry except for one row in which one is 0 and the other is 1 + - ``first_col_index1`` -- index of a column in ``first_mat`` identical to some + other column in every entry except for one row in which one is 0 and the other is 1 + - ``first_col_index2`` -- index of the other column which is identical to + first_mat[first_col_index1] in every entry except for one row in which + one is 0 and the other is 1 + - ``second_col_index1`` -- index of a column in ``second_mat`` identical to some + other column in every entry except for one row in which one is 0 and the other is 1 + - ``first_col_index2`` -- index of the other column which is identical to + second_mat[second_col_index1] in every entry except for one row in which + one is 0 and the other is 1 + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 5, sparse=True), + ....: [[1, 1, 1, 1, 1], [1, 1, 1, 0, 0], + ....: [1, 0, 1, 1, 0], [0, 0, 0, 1, 1], + ....: [1, 1, 0, 0, 1]]); M + [1 1 1 1 1] + [1 1 1 0 0] + [1 0 1 1 0] + [0 0 0 1 1] + [1 1 0 0 1] + sage: M3 = Matrix_cmr_chr_sparse._three_sum(M, M, 0, 1, 0, 1); M3 + [1 1 1 1 1 0] + [1 0 0 1 1 0] + [0 1 1 0 0 0] + [0 0 1 1 1 0] + [1 1 0 1 1 1] + [1 1 0 1 0 0] + [0 0 0 0 1 1] + [1 1 0 0 0 1] + """ + fc = len(first_mat.columns()) + sc = len(second_mat.columns()) + fr = len(first_mat.rows()) + sr = len(second_mat.rows()) + if any([fc < 3, sc < 3, fr < 2, sr < 2]): + raise ValueError('Some matrix is not large enough to perform a 3-sum') + if any([first_col_index1 >= fc, first_col_index2 >= fc, second_col_index1 >= sc, second_col_index2 >= sc]): + raise ValueError('Some column indicated exceeds its matrix size') + first_col1 = first_mat.columns()[first_col_index1] + first_col2 = first_mat.columns()[first_col_index2] + second_col1 = second_mat.columns()[second_col_index1] + second_col2 = second_mat.columns()[second_col_index2] + fir_nrows = range(fr) + sec_nrows = range(sr) + valid1 = False + valid2 = False + for i in fir_nrows: + if (first_col1[i] == 1 and first_col2[i] == 0) or (first_col1[i] == 0 and first_col2[i] == 1): + subcol1 = tuple(first_col1[k] for k in fir_nrows if k != i) + subcol2 = tuple(first_col2[k] for k in fir_nrows if k != i) + if subcol1 == subcol2: + valid1 = True + if i == fr: + first_row_index = i - 1 + else: + first_row_index = i + break + for i in sec_nrows: + if (second_col1[i] == 1 and second_col2[i] == 0) or (second_col1[i] == 0 and second_col2[i] == 1): + subcol1 = tuple(second_col1[k] for k in sec_nrows if k != i) + subcol2 = tuple(second_col2[k] for k in sec_nrows if k != i) + if subcol1 == subcol2: + valid2 = True + if i == sr: + second_row_index = i - 1 + else: + second_row_index = i + break + if not (valid1 and valid2): + raise ValueError('indicated columns of Matrices are not of appropriate form for 3-sum') + first_subcol = first_mat.delete_rows([first_row_index]).columns()[first_col_index1] + second_subcol = second_mat.delete_rows([second_row_index]).columns()[second_col_index1] + first_submat = first_mat.delete_columns([first_col_index1, first_col_index2]) + second_submat = second_mat.delete_columns([second_col_index1, second_col_index2]) + first_row = first_submat.rows()[first_row_index] + second_row = second_submat.rows()[second_row_index] + first_submat = first_submat.delete_rows([first_row_index]) + second_submat = second_submat.delete_rows([second_row_index]) + first_subrows = first_submat.rows() + second_subrows = second_submat.rows() + upper_right_rows = first_subcol.tensor_product(second_row).rows() + lower_left_rows = second_subcol.tensor_product(first_row).rows() + n1 = len(first_submat.rows()) + n2 = len(second_submat.rows()) + row_list = [] + for i in range(n1): + r = list(first_subrows[i]) + u = list(upper_right_rows[i]) + r.extend(u) + row_list.append(r) + for i in range(n2): + r = list(lower_left_rows[i]) + u = list(second_subrows[i]) + r.extend(u) + row_list.append(r) + return Matrix_cmr_chr_sparse._from_data(row_list, immutable=False) + + def delete_rows(self, indices): + r""" + Delete rows of the matrices ``self``. + """ + rows = self.rows() + n = len(rows) + row_list = [rows[i] for i in range(n) if i not in indices] + return Matrix_cmr_chr_sparse._from_data(row_list, immutable=False) + + def delete_columns(self, indices): + r""" + Delete columns of the matrices ``self``. + """ + rows = self.rows() + n = self.ncols() + row_list = [] + for i in indices: + if i >= n: + raise ValueError('Found index greater than matrix size') + for r in rows: + x = [] + for k in range(len(r)): + if not (k in indices): + x.append(r[k]) + row_list.append(x) + return Matrix_cmr_chr_sparse._from_data(row_list, immutable=False) + + def binary_pivot(self, row, column): + r""" + Apply a pivot to ``self`` and returns the resulting matrix. + Calculations are done over the binary field. + + Suppose a matrix is `\begin{bmatrix} 1 & c^T \\ b & D\end{bmatrix}`. + Then the pivot of the matrix with respect to `1` is + `\begin{bmatrix} 1 & c^T \\ b & D - bc^T\end{bmatrix}`. + + The terminology "pivot" is defined in [Sch1986]_, Ch. 19.4. + + .. SEEALSO:: :meth:`binary_pivots`, :meth:`ternary_pivot`, :meth:`ternary_pivots` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 10, 10, sparse=True), [ + ....: [1, 1, 0, 0, 0, 1, 0, 1, 0, 0], + ....: [1, 0, 0, 0, 0, 1, 1, 0, 1, 0], + ....: [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], + ....: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0], + ....: [0, 0, 1, 1, 0, 0, 0, 0, 0, 0], + ....: [0, 1, 1, 0, 0, 0, 0, 0, 0, 0], + ....: [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + ....: [0, 0, 0, 0, 0, 1, 0, 0, 0, 1], + ....: [1, 0, 0, 0, 0, 0, 1, 0, 1, 1], + ....: [1, 1, 0, 0, 0, 1, 0, 0, 0, 0] + ....: ]); M + [1 1 0 0 0 1 0 1 0 0] + [1 0 0 0 0 1 1 0 1 0] + [0 0 0 0 1 1 0 0 0 0] + [0 0 0 1 1 0 0 0 0 0] + [0 0 1 1 0 0 0 0 0 0] + [0 1 1 0 0 0 0 0 0 0] + [0 0 0 0 0 0 0 0 1 0] + [0 0 0 0 0 1 0 0 0 1] + [1 0 0 0 0 0 1 0 1 1] + [1 1 0 0 0 1 0 0 0 0] + sage: M.binary_pivot(0, 0) + [1 1 0 0 0 1 0 1 0 0] + [1 1 0 0 0 0 1 1 1 0] + [0 0 0 0 1 1 0 0 0 0] + [0 0 0 1 1 0 0 0 0 0] + [0 0 1 1 0 0 0 0 0 0] + [0 1 1 0 0 0 0 0 0 0] + [0 0 0 0 0 0 0 0 1 0] + [0 0 0 0 0 1 0 0 0 1] + [1 1 0 0 0 1 1 1 1 1] + [1 0 0 0 0 0 0 1 0 0] + """ + cdef Matrix_cmr_chr_sparse result + cdef size_t pivot_row = row + cdef size_t pivot_column = column + cdef CMR_CHRMAT *result_mat + + CMR_CALL(CMRchrmatBinaryPivot(cmr, self._mat, pivot_row, pivot_column, &result_mat)) + result = Matrix_cmr_chr_sparse._from_cmr(result_mat) + return result + + def binary_pivots(self, rows, columns): + r""" + Apply a sequence of pivots to ``self`` and returns the resulting matrix. + Calculations are done over the binary field. + + .. SEEALSO:: :meth:`binary_pivot`, :meth:`ternary_pivot`, :meth:`ternary_pivots` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 10, 10, sparse=True), [ + ....: [1, 1, 0, 0, 0, 1, 0, 1, 0, 0], + ....: [1, 0, 0, 0, 0, 1, 1, 0, 1, 0], + ....: [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], + ....: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0], + ....: [0, 0, 1, 1, 0, 0, 0, 0, 0, 0], + ....: [0, 1, 1, 0, 0, 0, 0, 0, 0, 0], + ....: [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + ....: [0, 0, 0, 0, 0, 1, 0, 0, 0, 1], + ....: [1, 0, 0, 0, 0, 0, 1, 0, 1, 1], + ....: [1, 1, 0, 0, 0, 1, 0, 0, 0, 0] + ....: ]); M + [1 1 0 0 0 1 0 1 0 0] + [1 0 0 0 0 1 1 0 1 0] + [0 0 0 0 1 1 0 0 0 0] + [0 0 0 1 1 0 0 0 0 0] + [0 0 1 1 0 0 0 0 0 0] + [0 1 1 0 0 0 0 0 0 0] + [0 0 0 0 0 0 0 0 1 0] + [0 0 0 0 0 1 0 0 0 1] + [1 0 0 0 0 0 1 0 1 1] + [1 1 0 0 0 1 0 0 0 0] + sage: M.binary_pivots([5, 4, 3, 2], [2, 3, 4, 5]) + [1 0 1 1 1 1 0 1 0 0] + [1 1 1 1 1 1 1 0 1 0] + [0 1 1 1 1 1 0 0 0 0] + [0 1 1 1 1 0 0 0 0 0] + [0 1 1 1 0 0 0 0 0 0] + [0 1 1 0 0 0 0 0 0 0] + [0 0 0 0 0 0 0 0 1 0] + [0 1 1 1 1 1 0 0 0 1] + [1 0 0 0 0 0 1 0 1 1] + [1 0 1 1 1 1 0 0 0 0] + """ + npivots = len(rows) + if len(columns) != npivots: + raise ValueError("The pivot rows and columns must have the same length") + + cdef size_t* pivot_rows = sig_malloc(sizeof(size_t)*npivots) + cdef size_t* pivot_columns = sig_malloc(sizeof(size_t)*npivots) + cdef CMR_CHRMAT *result_mat + + for i in range(npivots): + pivot_rows[i] = rows[i] + pivot_columns[i] = columns[i] + + try: + CMR_CALL(CMRchrmatBinaryPivots(cmr, self._mat, npivots, pivot_rows, pivot_columns, &result_mat)) + return Matrix_cmr_chr_sparse._from_cmr(result_mat) + finally: + sig_free(pivot_rows) + sig_free(pivot_columns) + + def ternary_pivot(self, row, column): + r""" + Apply a pivot to ``self`` and returns the resulting matrix. + Calculations are done over the ternary field. + + Suppose a matrix is `\begin{bmatrix} \epsilon & c^T \\ b & D\end{bmatrix}`, + where `\epsilon\in\{\pm 1\}`. + Then the pivot of the matrix with respect to `\epsilon` is + `\begin{bmatrix} -\epsilon & \epsilon c^T \\ \epsilon b & D-\epsilon bc^T\end{bmatrix}`. + + The terminology "pivot" is defined in [Sch1986]_, Ch. 19.4. + + .. SEEALSO:: :meth:`binary_pivot`, :meth:`binary_pivots`, :meth:`ternary_pivots` + + EXAMPLES: + + Single pivot on a `1`-entry:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 10, 10, sparse=True), [ + ....: [ 1, 1, 0, 0, 0, -1, 0, 1, 0, 0], + ....: [-1, 0, 0, 0, 0, 1, 1, 0, 1, 0], + ....: [ 0, 0, 0, 0, 1, 1, 0, 0, 0, 0], + ....: [ 0, 0, 0, 1, 1, 0, 0, 0, 0, 0], + ....: [ 0, 0, 1, 1, 0, 0, 0, 0, 0, 0], + ....: [ 0, 1, 1, 0, 0, 0, 0, 0, 0, 0], + ....: [ 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + ....: [ 0, 0, 0, 0, 0, 1, 0, 0, 0, 1], + ....: [ 1, 0, 0, 0, 0, 0, 1, 0, 1, 1], + ....: [ 1, 1, 0, 0, 0, 1, 0, 0, 0, 0] + ....: ]); M + [ 1 1 0 0 0 -1 0 1 0 0] + [-1 0 0 0 0 1 1 0 1 0] + [ 0 0 0 0 1 1 0 0 0 0] + [ 0 0 0 1 1 0 0 0 0 0] + [ 0 0 1 1 0 0 0 0 0 0] + [ 0 1 1 0 0 0 0 0 0 0] + [ 0 0 0 0 0 0 0 0 1 0] + [ 0 0 0 0 0 1 0 0 0 1] + [ 1 0 0 0 0 0 1 0 1 1] + [ 1 1 0 0 0 1 0 0 0 0] + sage: M.ternary_pivot(0, 0) + [-1 1 0 0 0 -1 0 1 0 0] + [-1 1 0 0 0 0 1 1 1 0] + [ 0 0 0 0 1 1 0 0 0 0] + [ 0 0 0 1 1 0 0 0 0 0] + [ 0 0 1 1 0 0 0 0 0 0] + [ 0 1 1 0 0 0 0 0 0 0] + [ 0 0 0 0 0 0 0 0 1 0] + [ 0 0 0 0 0 1 0 0 0 1] + [ 1 -1 0 0 0 1 1 -1 1 1] + [ 1 0 0 0 0 -1 0 -1 0 0] + + Single pivot on a `-1`-entry:: + + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 10, 10, sparse=True), [ + ....: [-1, 1, 0, 0, 0, -1, 0, 1, 0, 0], + ....: [-1, 0, 0, 0, 0, 1, 1, 0, 1, 0], + ....: [ 0, 0, 0, 0, 1, 1, 0, 0, 0, 0], + ....: [ 0, 0, 0, 1, 1, 0, 0, 0, 0, 0], + ....: [ 0, 0, 1, 1, 0, 0, 0, 0, 0, 0], + ....: [ 0, 1, 1, 0, 0, 0, 0, 0, 0, 0], + ....: [ 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + ....: [ 0, 0, 0, 0, 0, 1, 0, 0, 0, 1], + ....: [ 1, 0, 0, 0, 0, 0, 1, 0, 1, 1], + ....: [ 1, 1, 0, 0, 0, 1, 0, 0, 0, 0] + ....: ]); M + [-1 1 0 0 0 -1 0 1 0 0] + [-1 0 0 0 0 1 1 0 1 0] + [ 0 0 0 0 1 1 0 0 0 0] + [ 0 0 0 1 1 0 0 0 0 0] + [ 0 0 1 1 0 0 0 0 0 0] + [ 0 1 1 0 0 0 0 0 0 0] + [ 0 0 0 0 0 0 0 0 1 0] + [ 0 0 0 0 0 1 0 0 0 1] + [ 1 0 0 0 0 0 1 0 1 1] + [ 1 1 0 0 0 1 0 0 0 0] + sage: M.ternary_pivot(0, 0) + [ 1 -1 0 0 0 1 0 -1 0 0] + [ 1 -1 0 0 0 -1 1 -1 1 0] + [ 0 0 0 0 1 1 0 0 0 0] + [ 0 0 0 1 1 0 0 0 0 0] + [ 0 0 1 1 0 0 0 0 0 0] + [ 0 1 1 0 0 0 0 0 0 0] + [ 0 0 0 0 0 0 0 0 1 0] + [ 0 0 0 0 0 1 0 0 0 1] + [-1 1 0 0 0 -1 1 1 1 1] + [-1 -1 0 0 0 0 0 1 0 0] + """ + cdef size_t pivot_row = row + cdef size_t pivot_column = column + cdef CMR_CHRMAT *result_mat + + CMR_CALL(CMRchrmatTernaryPivot(cmr, self._mat, pivot_row, pivot_column, &result_mat)) + return Matrix_cmr_chr_sparse._from_cmr(result_mat) + + def ternary_pivots(self, rows, columns): + r""" + Apply a sequence of pivots to ``self`` and returns the resulting matrix. + Calculations are done over the ternary field. + + .. SEEALSO:: :meth:`binary_pivot`, :meth:`binary_pivots`, :meth:`ternary_pivot` + """ + cdef size_t npivots = len(rows) + if len(columns) != npivots: + raise ValueError("The pivot rows and columns must have the same length") + + cdef size_t* pivot_rows = sig_malloc(sizeof(size_t)*npivots) + cdef size_t* pivot_columns = sig_malloc(sizeof(size_t)*npivots) + cdef CMR_CHRMAT *result_mat + + for i in range(npivots): + pivot_rows[i] = rows[i] + pivot_columns[i] = columns[i] + + try: + CMR_CALL(CMRchrmatTernaryPivots(cmr, self._mat, npivots, pivot_rows, pivot_columns, &result_mat)) + return Matrix_cmr_chr_sparse._from_cmr(result_mat) + finally: + sig_free(pivot_rows) + sig_free(pivot_columns) + + def is_unimodular(self, time_limit=60.0): + r""" + Return whether ``self`` is a unimodular matrix. + + A nonsingular square matrix `A` is called unimodular if it is integral + and has determinant `\pm1`, i.e., an element of + `\mathop{\operatorname{GL}}_n(\ZZ)` [Sch1986]_, Ch. 4.3. + + A rectangular matrix `A` of full row rank is called unimodular if it + is integral and every basis `B` of `A` has determinant `\pm1`. + [Sch1986]_, Ch. 19.1. + + More generally, a matrix `A` of rank `r` is called unimodular if it is + integral and for every submatrix `B` formed by `r` linearly independent columns, + the greatest common divisor of the determinants of all `r`-by-`r` + submatrices of `B` is `1`. [Sch1986]_, Ch. 21.4. + + .. SEEALSO:: :meth:`is_k_equimodular`, :meth:`is_strongly_unimodular`, :meth:`is_totally_unimodular` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 2, 3, sparse=True), + ....: [[1, 0, 0], [0, 1, 0]]); M + [1 0 0] + [0 1 0] + sage: M.is_unimodular() + True + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 2, 3, sparse=True), + ....: [[1, 1, 0], [-1, 1, 1]]); M + [ 1 1 0] + [-1 1 1] + sage: M.is_unimodular() + False + """ + base_ring = self.parent().base_ring() + if base_ring.characteristic(): + raise ValueError(f'only defined over characteristic 0, got {base_ring}') + + cdef CMR_INTMAT *int_mat = NULL + cdef bool result + + sig_on() + try: + CMR_CALL(CMRchrmatToInt(cmr, self._mat, &int_mat)) + CMR_CALL(CMRunimodularTest(cmr, int_mat, &result, NULL, NULL, time_limit)) + finally: + CMR_CALL(CMRintmatFree(cmr, &int_mat)) + sig_off() + + return result + + def is_strongly_unimodular(self, time_limit=60.0): + r""" + Return whether ``self`` is a strongly unimodular matrix. + + A matrix is strongly unimodular if ``self`` and ``self.transpose()`` are both unimodular. + + .. SEEALSO:: meth:`is_unimodular`, :meth:`is_strongly_k_modular` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 3, sparse=True), + ....: [[1, 0, 1], [0, 1, 1], [1, 2, 3]]); M + [1 0 1] + [0 1 1] + [1 2 3] + sage: M.is_unimodular() + True + sage: M.is_strongly_unimodular() + False + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 2, 3, sparse=True), + ....: [[1, 0, 0], [0, 1, 0]]); M + [1 0 0] + [0 1 0] + sage: M.is_strongly_unimodular() + True + """ + base_ring = self.parent().base_ring() + if base_ring.characteristic(): + raise ValueError(f'only defined over characteristic 0, got {base_ring}') + + cdef CMR_INTMAT *int_mat = NULL + cdef bool result + + sig_on() + try: + CMR_CALL(CMRchrmatToInt(cmr, self._mat, &int_mat)) + CMR_CALL(CMRunimodularTestStrong(cmr, int_mat, &result, NULL, NULL, time_limit)) + finally: + CMR_CALL(CMRintmatFree(cmr, &int_mat)) + sig_off() + + return result + + def equimodulus(self, time_limit=60.0): + r""" + Return the integer `k` such that ``self`` is + equimodular with determinant gcd `k`. + + A matrix `M` of rank `r` is equimodular with determinant gcd `k` + if the following two conditions are satisfied: + + - for some column basis `B` of `M`, the greatest common divisor of + the determinants of all `r`-by-`r` submatrices of `B` is `k`; + + - the matrix `X` such that `M=BX` is totally unimodular. + + OUTPUT: + + - ``k``: ``self`` is equimodular with determinant gcd `k` + - ``None``: ``self`` is not equimodular for any `k` + + .. SEEALSO:: :meth:`is_k_equimodular`, :meth:`strong_equimodulus` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 3, sparse=True), + ....: [[1, 0, 1], [0, 1, 1], [1, 2, 3]]); M + [1 0 1] + [0 1 1] + [1 2 3] + sage: M.equimodulus() + 1 + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 2, 3, sparse=True), + ....: [[1, 1, 1], [0, 1, 3]]); M + [1 1 1] + [0 1 3] + sage: M.equimodulus() + """ + cdef CMR_INTMAT *int_mat = NULL + cdef bool result + cdef int64_t k = 0 + + sig_on() + try: + CMR_CALL(CMRchrmatToInt(cmr, self._mat, &int_mat)) + CMR_CALL(CMRequimodularTest(cmr, int_mat, &result, &k, NULL, NULL, time_limit)) + finally: + CMR_CALL(CMRintmatFree(cmr, &int_mat)) + sig_off() + + if result: + return k + else: + return None + + def strong_equimodulus(self, time_limit=60.0): + r""" + Return the integer `k` such that ``self`` is + strongly equimodular with determinant gcd `k`. + + Return whether ``self`` is strongly `k`-equimodular. + + A matrix is strongly equimodular if ``self`` and ``self.transpose()`` + are both equimodular, which implies that they are equimodular for + the same determinant gcd `k`. + A matrix `M` of rank-`r` is `k`-equimodular if the following two conditions + are satisfied: + + - for some column basis `B` of `M`, the greatest common divisor of the + determinants of all `r`-by-`r` submatrices of `B` is `k`; + + - the matrix `X` such that `M=BX` is totally unimodular. + + OUTPUT: + + - ``k``: ``self`` is `k`-equimodular + - ``None``: ``self`` is not `k`-equimodular for any `k` + + .. SEEALSO:: :meth:`is_strongly_k_equimodular`, :meth:`equimodulus` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 3, sparse=True), + ....: [[1, 0, 1], [0, 1, 1], [1, 2, 3]]); M + [1 0 1] + [0 1 1] + [1 2 3] + sage: M.strong_equimodulus() + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 2, 3, sparse=True), + ....: [[1, 0, 0], [0, 1, 0]]); M + [1 0 0] + [0 1 0] + sage: M.strong_equimodulus() + 1 + """ + cdef CMR_INTMAT *int_mat = NULL + cdef bool result + cdef int64_t k = 0 + + sig_on() + try: + CMR_CALL(CMRchrmatToInt(cmr, self._mat, &int_mat)) + CMR_CALL(CMRequimodularTestStrong(cmr, int_mat, &result, &k, NULL, NULL, time_limit)) + finally: + CMR_CALL(CMRintmatFree(cmr, &int_mat)) + sig_off() + + if result: + return k + else: + return None + + def is_k_equimodular(self, k, time_limit=60.0): + r""" + Return whether ``self`` is equimodular with determinant gcd `k`. + + A matrix `M` of rank-`r` is `k`-equimodular if the following two + conditions are satisfied: + + - for some column basis `B` of `M`, the greatest common divisor of + the determinants of all `r`-by-`r` submatrices of `B` is `k`; + + - the matrix `X` such that `M=BX` is totally unimodular. + + If the matrix has full row rank, it is `k`-equimodular if + every full rank minor of the matrix has determinant `0,\pm k`. + + .. NOTE:: + + In parts of the literature, a matrix with the above properties + is called *strictly* `k`-modular. + + .. SEEALSO:: :meth:`is_unimodular`, :meth:`is_strongly_k_equimodular`, + :meth:`equimodulus` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 3, sparse=True), + ....: [[1, 0, 1], [0, 1, 1], [1, 2, 3]]); M + [1 0 1] + [0 1 1] + [1 2 3] + sage: M.is_k_equimodular(1) + True + sage: M.is_k_equimodular(2) + False + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 2, 3, sparse=True), + ....: [[1, 1, 1], [0, 1, 3]]); M + [1 1 1] + [0 1 3] + sage: M.is_k_equimodular(1) + False + """ + base_ring = self.parent().base_ring() + if base_ring.characteristic(): + raise ValueError(f'only defined over characteristic 0, got {base_ring}') + + cdef CMR_INTMAT *int_mat = NULL + cdef bool result + cdef int64_t gcd_det = k + + sig_on() + try: + CMR_CALL(CMRchrmatToInt(cmr, self._mat, &int_mat)) + CMR_CALL(CMRequimodularTest(cmr, int_mat, &result, &gcd_det, NULL, NULL, time_limit)) + finally: + CMR_CALL(CMRintmatFree(cmr, &int_mat)) + sig_off() + + return True if result else False + + def is_strongly_k_equimodular(self, k, time_limit=60.0): + r""" + Return whether ``self`` is strongly `k`-equimodular. + + A matrix is strongly `k`-equimodular if ``self`` and ``self.transpose()`` + are both `k`-equimodular. + + .. SEEALSO:: :meth:`is_k_equimodular`, :meth:`is_strongly_unimodular`, + :meth:`strong_equimodulus` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 3, sparse=True), + ....: [[1, 0, 1], [0, 1, 1], [1, 2, 3]]); M + [1 0 1] + [0 1 1] + [1 2 3] + sage: M.is_strongly_k_equimodular(1) + False + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 2, 3, sparse=True), + ....: [[1, 0, 0], [0, 1, 0]]); M + [1 0 0] + [0 1 0] + sage: M.is_strongly_k_equimodular(1) + True + """ + base_ring = self.parent().base_ring() + if base_ring.characteristic(): + raise ValueError(f'only defined over characteristic 0, got {base_ring}') + + cdef CMR_INTMAT *int_mat = NULL + cdef bool result + cdef int64_t gcd_det = k + + sig_on() + try: + CMR_CALL(CMRchrmatToInt(cmr, self._mat, &int_mat)) + CMR_CALL(CMRequimodularTestStrong(cmr, int_mat, &result, &gcd_det, NULL, NULL, time_limit)) + finally: + CMR_CALL(CMRintmatFree(cmr, &int_mat)) + sig_off() + + return True if result else False + + def _is_binary_linear_matroid_graphic(self, *, time_limit=60.0, decomposition=False, certificate=False, + row_keys=None, column_keys=None): + r""" + Return whether the linear matroid of ``self`` over `\GF{2}` is graphic. + If there is some entry not in `\{0, 1\}`, return ``False``. + + This is an internal method because it should really be exposed + as a method of :class:`Matroid`. + + Equivalently, we also define the graphic matrix as follows. + + Let `G = (V,E)` be a graph and let `T` be a spanning forest. + The matrix `M(G,T) \in \{0,1\}^{T \times (E \setminus T)}` defined via + ` + M(D,T)_{e, f} := \begin{cases} + 1 & \text{if $e$ is contained in the unique cycle of $T\cup\{f\}$}, \\ + 0 & \text{otherwise} + \end{cases} + ` + is called the graphic matrix of `G` with respect to `T`. + A binary matrix `M` is called graphic if there exists a graph `G` + with a spanning forest `T` such that `M = M(G,T)`. + Moreover, `M` is called cographic if `M^T` is graphic, and + it is called planar if it is graphic and cographic. + + .. SEEALSO:: + + :meth:`M._is_graphic_cmr() ` + :meth:`M.is_graphic() ` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 2, sparse=True), + ....: [[1, 0], [-1, 1], [0, -1]]); M + [ 1 0] + [-1 1] + [ 0 -1] + sage: M._is_binary_linear_matroid_graphic() + False + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 2, sparse=True), + ....: [[1, 0], [1, 1], [0, 1]]); M + [1 0] + [1 1] + [0 1] + sage: M._is_binary_linear_matroid_graphic() + True + sage: result, certificate = M._is_binary_linear_matroid_graphic(certificate=True) + sage: graph, forest_edges, coforest_edges = certificate + sage: graph.vertices(sort=True) # the numbers have no meaning + [1, 2, 7, 12] + sage: graph.edges(sort=True, labels=False) + [(1, 2), (1, 7), (1, 12), (2, 7), (7, 12)] + sage: forest_edges # indexed by rows of M + ((1, 2), (7, 1), (12, 7)) + sage: coforest_edges # indexed by cols of M + ((2, 7), (1, 12)) + + With keys:: + + sage: result, certificate = M._is_binary_linear_matroid_graphic(certificate=True, + ....: row_keys=['a', 'b', 'c'], column_keys=['v', 'w']) + sage: graph, forest_edges, coforest_edges = certificate + sage: forest_edges + {'a': (1, 2), 'b': (7, 1), 'c': (12, 7)} + sage: coforest_edges + {'v': (2, 7), 'w': (1, 12)} + + Creating a decomposition node:: + + sage: result, node = M._is_binary_linear_matroid_graphic(decomposition=True, + ....: row_keys=['a', 'b', 'c'], column_keys=['v', 'w']) + sage: result, node + (True, GraphicNode (3×2)) + + TESTS:: + + sage: M._is_binary_linear_matroid_graphic(time_limit=0.0) + Traceback (most recent call last): + ... + RuntimeError: Time limit exceeded + """ + base_ring = self.parent().base_ring() + from sage.rings.finite_rings.finite_field_constructor import GF + GF2 = GF(2) + if not GF2.has_coerce_map_from(base_ring): + raise ValueError('not well-defined') + + cdef bool result_bool + cdef CMR_GRAPH *graph = NULL + cdef CMR_GRAPH_EDGE* forest_edges = NULL + cdef CMR_GRAPH_EDGE* coforest_edges = NULL + cdef CMR_SUBMAT* submatrix = NULL + cdef CMR_GRAPHIC_STATISTICS stats + + sig_on() + try: + if decomposition or certificate: + CMR_CALL(CMRgraphicTestMatrix(cmr, self._mat, &result_bool, &graph, &forest_edges, + &coforest_edges, &submatrix, &stats, time_limit)) + else: + CMR_CALL(CMRgraphicTestMatrix(cmr, self._mat, &result_bool, NULL, NULL, + NULL, NULL, &stats, time_limit)) + finally: + sig_off() + + result = result_bool + + if not decomposition and not certificate: + return result + + if result: + result = [result] + sage_graph = _sage_graph(graph) + sage_forest_edges = _sage_edges(graph, forest_edges, self.nrows(), row_keys) + sage_coforest_edges = _sage_edges(graph, coforest_edges, self.ncols(), column_keys) + if decomposition: + result.append(GraphicNode(self, sage_graph, sage_forest_edges, sage_coforest_edges, + row_keys=row_keys, column_keys=column_keys)) + if certificate: + result.append((sage_graph, sage_forest_edges, sage_coforest_edges)) + else: + result = [result] + if decomposition: + raise NotImplementedError + if certificate: + result.append(NotImplemented) # submatrix TBD + return result + + def _is_binary_linear_matroid_cographic(self, *, time_limit=60.0, certificate=False, + row_keys=None, column_keys=None): + r""" + Return whether the linear matroid of ``self`` over `\GF{2}` is cographic. + If there is some entry not in `\{0, 1\}`, return ``False``. + + This is an internal method because it should really be exposed + as a method of :class:`Matroid`. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 4, 9, sparse=True), + ....: [[1, 0, 0, 0, 1, -1, 1, 0, 0], + ....: [0, 1, 0, 0, 0, 1, -1, 1, 0], + ....: [0, 0, 1, 0, 0, 0, 1, -1, 1], + ....: [0, 0, 0, 1, 1, 0, 0, 1, -1]]); M + [ 1 0 0 0 1 -1 1 0 0] + [ 0 1 0 0 0 1 -1 1 0] + [ 0 0 1 0 0 0 1 -1 1] + [ 0 0 0 1 1 0 0 1 -1] + sage: M._is_binary_linear_matroid_cographic() + False + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(GF(2), 4, 9, sparse=True), + ....: M); M + [1 0 0 0 1 1 1 0 0] + [0 1 0 0 0 1 1 1 0] + [0 0 1 0 0 0 1 1 1] + [0 0 0 1 1 0 0 1 1] + sage: M._is_binary_linear_matroid_cographic() + True + sage: C3 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 3, sparse=True), + ....: [[1, 1, 0], + ....: [1, 0, 1], + ....: [0, 1, 1]]); C3 + [1 1 0] + [1 0 1] + [0 1 1] + sage: result, certificate = C3._is_binary_linear_matroid_cographic(certificate=True) + sage: result + True + sage: graph, forest_edges, coforest_edges = certificate + sage: graph.edges(sort=True, labels=False) + [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)] + sage: forest_edges + ((3, 2), (0, 3), (1, 3)) + sage: coforest_edges + ((2, 0), (2, 1), (0, 1)) + """ + base_ring = self.parent().base_ring() + from sage.rings.finite_rings.finite_field_constructor import GF + GF2 = GF(2) + if not GF2.has_coerce_map_from(base_ring): + raise ValueError('not well-defined') + + cdef bool result + cdef CMR_GRAPH *graph = NULL + cdef CMR_GRAPH_EDGE* forest_edges = NULL + cdef CMR_GRAPH_EDGE* coforest_edges = NULL + cdef CMR_SUBMAT* submatrix = NULL + cdef CMR_GRAPHIC_STATISTICS stats + + sig_on() + try: + if certificate: + CMR_CALL(CMRgraphicTestTranspose(cmr, self._mat, &result, &graph, &forest_edges, + &coforest_edges, &submatrix, &stats, time_limit)) + else: + CMR_CALL(CMRgraphicTestTranspose(cmr, self._mat, &result, NULL, NULL, + NULL, NULL, &stats, time_limit)) + finally: + sig_off() + + if not certificate: + return result + + if result: + sage_graph = _sage_graph(graph) + sage_forest_edges = _sage_edges(graph, forest_edges, self.nrows(), row_keys) + sage_coforest_edges = _sage_edges(graph, coforest_edges, self.ncols(), column_keys) + return True, (sage_graph, sage_forest_edges, sage_coforest_edges) + + return False, NotImplemented # submatrix TBD + + def is_network_matrix(self, *, time_limit=60.0, certificate=False, + row_keys=None, column_keys=None): + r""" + Return whether the matrix ``self`` over `\GF{3}` or `\QQ` is a network matrix. + If there is some entry not in `\{-1, 0, 1\}`, return ``False``. + + Let `D = (V,A)` be a digraph and let `T` be an (arbitrarily) directed + spanning forest of the underlying undirected graph. + The matrix `M(D,T) \in \{-1,0,1\}^{T \times (A \setminus T)}` defined via + + .. MATH:: + + M(D,T)_{a,(v,w)} := \begin{cases} + +1 & \text{if the unique $v$-$w$-path in $T$ passes through $a$ forwardly}, \\ + -1 & \text{if the unique $v$-$w$-path in $T$ passes through $a$ backwardly}, \\ + 0 & \text{otherwise} + \end{cases} + + is called the network matrix of `D` with respect to `T`. + A matrix `M` is called network matrix if there exists a digraph `D` + with a directed spanning forest `T` such that `M = M(D,T)`. + Moreover, `M` is called conetwork matrix if `M^T` is a network matrix. + + ALGORITHM: + + The implemented recognition algorithm first tests the binary matroid of + the support matrix of `M` for being graphic and + uses camion for testing whether `M` is signed correctly. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 2, sparse=True), + ....: [[1, 0], [2, 1], [0, -1]]); M + [ 1 0] + [ 2 1] + [ 0 -1] + sage: M.is_network_matrix() + False + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 2, sparse=True), + ....: [[1, 0], [-1, 1], [0, -1]]); M + [ 1 0] + [-1 1] + [ 0 -1] + sage: M.is_network_matrix() + True + sage: result, certificate = M.is_network_matrix(certificate=True) + sage: graph, forest_edges, coforest_edges = certificate + sage: graph + Digraph on 4 vertices + sage: graph.vertices(sort=True) # the numbers have no meaning + [1, 2, 7, 12] + sage: graph.edges(sort=True, labels=False) + [(2, 1), (2, 7), (7, 1), (7, 12), (12, 1)] + sage: forest_edges # indexed by rows of M + ((2, 1), (7, 1), (7, 12)) + sage: coforest_edges # indexed by cols of M + ((2, 7), (12, 1)) + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: K33 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 4, sparse=True), + ....: [[-1, -1, -1, -1], + ....: [ 1, 1, 0, 0], + ....: [ 0, 0, 1, 1], + ....: [ 1, 0, 1, 0], + ....: [ 0, 1, 0, 1]]); K33 + [-1 -1 -1 -1] + [ 1 1 0 0] + [ 0 0 1 1] + [ 1 0 1 0] + [ 0 1 0 1] + sage: K33.is_network_matrix() + True + + This is test ``Basic`` in CMR's ``test_network.cpp``:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 6, 7, sparse=True), + ....: [[-1, 0, 0, 0, 1, -1, 0], + ....: [ 1, 0, 0, 1, -1, 1, 0], + ....: [ 0, -1, 0, -1, 1, -1, 0], + ....: [ 0, 1, 0, 0, 0, 0, 1], + ....: [ 0, 0, 1, -1, 1, 0, 1], + ....: [ 0, 0, -1, 1, -1, 0, 0]]) + sage: M.is_network_matrix() + True + sage: result, certificate = M.is_network_matrix(certificate=True) + sage: result, certificate + (True, + (Digraph on 7 vertices, + ((9, 8), (3, 8), (3, 4), (5, 4), (4, 6), (0, 6)), + ((3, 9), (5, 3), (4, 0), (0, 8), (9, 0), (4, 9), (5, 6)))) + sage: digraph, forest_arcs, coforest_arcs = certificate + sage: list(digraph.edges(sort=True)) + [(0, 6, None), (0, 8, None), + (3, 4, None), (3, 8, None), (3, 9, None), + (4, 0, None), (4, 6, None), (4, 9, None), + (5, 3, None), (5, 4, None), (5, 6, None), + (9, 0, None), (9, 8, None)] + sage: digraph.plot(edge_colors={'red': forest_arcs}) # needs sage.plot + Graphics object consisting of 21 graphics primitives + """ + base_ring = self.parent().base_ring() + if base_ring.characteristic() not in [0, 3] : + raise ValueError(f'only defined over characteristic 0 or 3, got {base_ring}') + + cdef bool result + cdef bool support_result + cdef CMR_GRAPH *digraph = NULL + cdef CMR_GRAPH_EDGE* forest_arcs = NULL + cdef CMR_GRAPH_EDGE* coforest_arcs = NULL + cdef bool* arcs_reversed = NULL + cdef CMR_SUBMAT* submatrix = NULL + cdef CMR_NETWORK_STATISTICS stats + + sig_on() + try: + if certificate: + CMR_CALL(CMRnetworkTestMatrix(cmr, self._mat, &result, &support_result, &digraph, &forest_arcs, + &coforest_arcs, &arcs_reversed, &submatrix, &stats, + time_limit)) + else: + CMR_CALL(CMRnetworkTestMatrix(cmr, self._mat, &result, &support_result, NULL, NULL, + NULL, NULL, NULL, &stats, time_limit)) + finally: + sig_off() + + if not certificate: + return result + + if result: + sage_digraph = _sage_digraph(digraph, arcs_reversed) + sage_forest_arcs = _sage_arcs(digraph, forest_arcs, arcs_reversed, self.nrows(), row_keys) + sage_coforest_arcs = _sage_arcs(digraph, coforest_arcs, arcs_reversed, self.ncols(), column_keys) + return True, (sage_digraph, sage_forest_arcs, sage_coforest_arcs) + + return False, NotImplemented # submatrix TBD + + def is_conetwork_matrix(self, *, time_limit=60.0, certificate=False, + row_keys=None, column_keys=None): + r""" + Return whether the matrix ``self`` over `\GF{3}` or `QQ` is a conetwork matrix. + If there is some entry not in `\{-1, 0, 1\}`, return ``False``. + + A matrix is conetwork if and only if its transpose is network. + + .. SEEALSO:: :meth:`is_network_matrix`, + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 4, 9, sparse=True), + ....: [[1, 0, 0, 0, 1, -1, 1, 0, 0], + ....: [0, 1, 0, 0, 0, 1, -1, 1, 0], + ....: [0, 0, 1, 0, 0, 0, 1, -1, 1], + ....: [0, 0, 0, 1, 1, 0, 0, 1, -1]]); M + [ 1 0 0 0 1 -1 1 0 0] + [ 0 1 0 0 0 1 -1 1 0] + [ 0 0 1 0 0 0 1 -1 1] + [ 0 0 0 1 1 0 0 1 -1] + sage: M.is_conetwork_matrix() + True + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: K33 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 4, sparse=True), + ....: [[-1, -1, -1, -1], + ....: [ 1, 1, 0, 0], + ....: [ 0, 0, 1, 1], + ....: [ 1, 0, 1, 0], + ....: [ 0, 1, 0, 1]]); K33 + [-1 -1 -1 -1] + [ 1 1 0 0] + [ 0 0 1 1] + [ 1 0 1 0] + [ 0 1 0 1] + sage: K33.is_conetwork_matrix() + False + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: C3 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 3, sparse=True), + ....: [[1, 1, 0], + ....: [1, 0, 1], + ....: [0, 1, 1]]); C3 + [1 1 0] + [1 0 1] + [0 1 1] + sage: result, certificate = C3.is_conetwork_matrix(certificate=True) + sage: result + False + """ + base_ring = self.parent().base_ring() + if base_ring.characteristic() not in [0, 3] : + raise ValueError(f'only defined over characteristic 0 or 3, got {base_ring}') + + cdef bool result + cdef bool support_result + cdef CMR_GRAPH *digraph = NULL + cdef CMR_GRAPH_EDGE* forest_arcs = NULL + cdef CMR_GRAPH_EDGE* coforest_arcs = NULL + cdef bool* arcs_reversed = NULL + cdef CMR_SUBMAT* submatrix = NULL + cdef CMR_NETWORK_STATISTICS stats + + sig_on() + try: + if certificate: + CMR_CALL(CMRnetworkTestTranspose(cmr, self._mat, &result, &support_result, &digraph, &forest_arcs, + &coforest_arcs, &arcs_reversed, &submatrix, &stats, + time_limit)) + else: + CMR_CALL(CMRnetworkTestTranspose(cmr, self._mat, &result, &support_result, NULL, NULL, + NULL, NULL, NULL, &stats, time_limit)) + finally: + sig_off() + + if not certificate: + return result + + if result: + sage_digraph = _sage_digraph(digraph, arcs_reversed) + sage_forest_arcs = _sage_arcs(digraph, forest_arcs, arcs_reversed, self.nrows(), row_keys) + sage_coforest_arcs = _sage_arcs(digraph, coforest_arcs, arcs_reversed, self.ncols(), column_keys) + return True, (sage_digraph, sage_forest_arcs, sage_coforest_arcs) + + return False, NotImplemented # submatrix TBD + + def _is_binary_linear_matroid_regular(self, *, time_limit=60.0, certificate=False, + use_direct_graphicness_test=True, + prefer_graphicness=True, + series_parallel_ok=True, + check_graphic_minors_planar=False, + stop_when_irregular=True, + three_sum_pivot_children=False, + three_sum_strategy=None, + construct_leaf_graphs=False, + construct_all_graphs=False, + row_keys=None, + column_keys=None): + r""" + Return whether the linear matroid of ``self`` over `\GF{2}` is regular. + If there is some entry not in `\{0, 1\}`, return ``False``. + + This is an internal method because it should really be exposed + as a method of :class:`Matroid`. + + .. SEEALSO:: + + :meth:`M.is_regular() ` + + INPUT: + + - ``certificate``: ``False`` or ``True`` + If ``True``, then return a :class:`DecompositionNode` + if the linear matroid of ``self`` over `\GF{2}` is regular; + If not, NotImplemented. + + - ``stop_when_irregular`` -- boolean (default: ``True``); + whether to stop decomposing once irregularity is determined. + + For a description of other parameters, see :meth:`_set_cmr_seymour_parameters` + + - ``row_keys`` -- a finite or enumerated family of arbitrary objects + that index the rows of the matrix + + - ``column_keys`` -- a finite or enumerated family of arbitrary objects + that index the columns of the matrix + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 2, sparse=True), + ....: [[1, 0], [-1, 1], [0, -1]]); M + [ 1 0] + [-1 1] + [ 0 -1] + sage: M._is_binary_linear_matroid_regular() + False + sage: M = Matrix_cmr_chr_sparse(M.parent().change_ring(GF(2)), + ....: M); M + [1 0] + [1 1] + [0 1] + sage: M._is_binary_linear_matroid_regular() + True + + sage: MF = matroids.catalog.Fano(); MF + Fano: Binary matroid of rank 3 on 7 elements, type (3, 0) + sage: MFR = MF.representation().change_ring(ZZ); MFR + [1 0 0 0 1 1 1] + [0 1 0 1 0 1 1] + [0 0 1 1 1 0 1] + sage: MFR2 = block_diagonal_matrix(MFR, MFR, sparse=True); MFR2 + [1 0 0 0 1 1 1|0 0 0 0 0 0 0] + [0 1 0 1 0 1 1|0 0 0 0 0 0 0] + [0 0 1 1 1 0 1|0 0 0 0 0 0 0] + [-------------+-------------] + [0 0 0 0 0 0 0|1 0 0 0 1 1 1] + [0 0 0 0 0 0 0|0 1 0 1 0 1 1] + [0 0 0 0 0 0 0|0 0 1 1 1 0 1] + sage: MS2 = MFR2.parent(); MS2 + Full MatrixSpace of 6 by 14 sparse matrices over Integer Ring + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: MFR2cmr = Matrix_cmr_chr_sparse(MS2, MFR2) + sage: result, certificate = MFR2cmr._is_binary_linear_matroid_regular( + ....: certificate=True) + sage: result, certificate + (False, (OneSumNode (6×14) with 2 children, NotImplemented)) + sage: certificate[0].child_indices() + (((0, 1, 2), (0, 4, 5, 6, 2, 3, 1)), ((3, 4, 5), (7, 11, 12, 13, 9, 10, 8))) + sage: unicode_art(certificate[0]) # random (whether the left or the right branch has been followed) + ╭OneSumNode (6×14) with 2 children╮ + │ │ + SeriesParallelReductionNode (3×7) UnknownNode (3×7) + │ + ThreeConnectedIrregularNode (3×4) + sage: result, certificate = MFR2cmr._is_binary_linear_matroid_regular( + ....: certificate=True) + sage: result, certificate + (False, (OneSumNode (6×14) with 2 children, NotImplemented)) + sage: unicode_art(certificate[0]) + ╭OneSumNode (6×14) with 2 children╮ + │ │ + SeriesParallelReductionNode (3×7) UnknownNode (3×7) + │ + ThreeConnectedIrregularNode (3×4) + sage: result, certificate = MFR2cmr._is_binary_linear_matroid_regular( + ....: certificate=True, stop_when_irregular=False) + sage: result, certificate + (False, (OneSumNode (6×14) with 2 children, NotImplemented)) + sage: unicode_art(certificate[0]) + ╭OneSumNode (6×14) with 2 children╮ + │ │ + SeriesParallelReductionNode (3×7) SeriesParallelReductionNode (3×7) + │ │ + ThreeConnectedIrregularNode (3×4) ThreeConnectedIrregularNode (3×4) + + TESTS: + + This is test ``NestedMinorPivotsTwoSeparation`` in CMR's ``test_regular.cpp``:: + + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 11, 11, sparse=True), + ....: [[1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + ....: [1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0], + ....: [0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], + ....: [0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0], + ....: [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1], + ....: [0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0], + ....: [0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0], + ....: [0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0], + ....: [0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0], + ....: [0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0], + ....: [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1]]) + sage: result, certificate = M._is_binary_linear_matroid_regular( + ....: certificate=True) + sage: result, certificate + (True, GraphicNode (11×11)) + sage: unicode_art(certificate) + GraphicNode (11×11) + sage: result, certificate = M._is_binary_linear_matroid_regular( + ....: certificate=True, + ....: use_direct_graphicness_test=False) + sage: result, certificate + (True, TwoSumNode (11×11) with 2 children) + sage: unicode_art(certificate) + ╭──────────TwoSumNode (11×11) with 2 children + │ │ + GraphicNode (7×8) SeriesParallelReductionNode (5×4) + │ + GraphicNode (4×4) + + Base ring check:: + + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(GF(5), 3, 2, sparse=True), + ....: [[1, 0], [6, 11], [0, 1]]); M + [1 0] + [1 1] + [0 1] + sage: M._is_binary_linear_matroid_regular() + Traceback (most recent call last): + ... + ValueError: not well-defined + + """ + base_ring = self.parent().base_ring() + from sage.rings.finite_rings.finite_field_constructor import GF + GF2 = GF(2) + if not GF2.has_coerce_map_from(base_ring): + raise ValueError('not well-defined') + + cdef bool result_bool + cdef CMR_REGULAR_PARAMS params + cdef CMR_REGULAR_STATS stats + cdef CMR_SEYMOUR_NODE *dec = NULL + cdef CMR_MINOR *minor = NULL + + cdef CMR_SEYMOUR_NODE **pdec = &dec + cdef CMR_MINOR **pminor = &minor + + cdef dict kwds = dict(use_direct_graphicness_test=use_direct_graphicness_test, + prefer_graphicness=prefer_graphicness, + series_parallel_ok=series_parallel_ok, + check_graphic_minors_planar=check_graphic_minors_planar, + stop_when_irregular=stop_when_irregular, + stop_when_nongraphic=False, + stop_when_noncographic=False, + stop_when_nongraphic_and_noncographic=False, + three_sum_pivot_children=three_sum_pivot_children, + three_sum_strategy=three_sum_strategy, + construct_leaf_graphs=construct_leaf_graphs, + construct_all_graphs=construct_all_graphs) + + _set_cmr_seymour_parameters(¶ms.seymour, kwds) + sig_on() + try: + CMR_CALL(CMRregularTest(cmr, self._mat, &result_bool, pdec, pminor, + ¶ms, &stats, time_limit)) + finally: + sig_off() + + result = result_bool + if not certificate: + return result + node = create_DecompositionNode(dec, self, row_keys, column_keys, base_ring=GF2) + + if result: + return result, node + return result, (node, NotImplemented) + + def is_totally_unimodular(self, *, time_limit=60.0, certificate=False, + use_direct_graphicness_test=True, + prefer_graphicness=True, + series_parallel_ok=True, + check_graphic_minors_planar=False, + stop_when_nonTU=True, + three_sum_pivot_children=False, + three_sum_strategy=None, + construct_leaf_graphs=False, + construct_all_graphs=False, + row_keys=None, + column_keys=None): + r""" + Return whether ``self`` is a totally unimodular matrix. + + A matrix is totally unimodular if every subdeterminant is `0`, `1`, or `-1`. + + REFERENCES: + + - [Sch1986]_, Chapter 19 + + INPUT: + + - ``certificate`` -- boolean (default: ``False``); + if ``True``, then return + a :class:`DecompositionNode` if ``self`` is totally unimodular; + a submatrix with determinant not in `\{0, \pm1\}` if not. + + - ``stop_when_nonTU`` -- boolean (default: ``True``); + whether to stop decomposing once not TU is determined. + + For a description of other parameters, see :meth:`_set_cmr_seymour_parameters` + + - ``row_keys`` -- a finite or enumerated family of arbitrary objects + that index the rows of the matrix + + - ``column_keys`` -- a finite or enumerated family of arbitrary objects + that index the columns of the matrix + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 2, sparse=True), + ....: [[1, 0], [2, 1], [0, 1]]); M + [1 0] + [2 1] + [0 1] + sage: M.is_totally_unimodular() + False + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 2, sparse=True), + ....: [[1, 0], [-1, 1], [0, 1]]); M + [ 1 0] + [-1 1] + [ 0 1] + sage: M.is_totally_unimodular() + True + sage: M.is_totally_unimodular(certificate=True) + (True, GraphicNode (3×2)) + + sage: MF = matroids.catalog.Fano(); MF + Fano: Binary matroid of rank 3 on 7 elements, type (3, 0) + sage: MFR = MF.representation().change_ring(ZZ); MFR + [1 0 0 0 1 1 1] + [0 1 0 1 0 1 1] + [0 0 1 1 1 0 1] + sage: MFR2 = block_diagonal_matrix(MFR, MFR, sparse=True); MFR2 + [1 0 0 0 1 1 1|0 0 0 0 0 0 0] + [0 1 0 1 0 1 1|0 0 0 0 0 0 0] + [0 0 1 1 1 0 1|0 0 0 0 0 0 0] + [-------------+-------------] + [0 0 0 0 0 0 0|1 0 0 0 1 1 1] + [0 0 0 0 0 0 0|0 1 0 1 0 1 1] + [0 0 0 0 0 0 0|0 0 1 1 1 0 1] + sage: MS2 = MFR2.parent(); MS2 + Full MatrixSpace of 6 by 14 sparse matrices over Integer Ring + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: MFR2cmr = Matrix_cmr_chr_sparse(MS2, MFR2) + sage: MFR2cmr.is_totally_unimodular(certificate=True) + (False, (OneSumNode (6×14) with 2 children, ((2, 1, 0), (5, 4, 3)))) + sage: result, certificate = MFR2cmr.is_totally_unimodular(certificate=True, + ....: stop_when_nonTU=True) + sage: result, certificate + (False, (OneSumNode (6×14) with 2 children, ((2, 1, 0), (5, 4, 3)))) + sage: submatrix = MFR2.matrix_from_rows_and_columns(*certificate[1]); submatrix + [0 1 1] + [1 0 1] + [1 1 0] + sage: submatrix.determinant() + 2 + sage: submatrix = MFR2cmr.matrix_from_rows_and_columns(*certificate[1]); submatrix + [0 1 1] + [1 0 1] + [1 1 0] + + If the matrix is totally unimodular, it always returns + a full decomposition as a certificate:: + + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 9, 9, sparse=True), + ....: [[-1,-1,-1,-1, 0, 0, 0, 0, 0], + ....: [1, 1, 0, 0, 0, 0, 0, 0, 0], + ....: [0, 0, 1, 1, 0, 0, 0, 0, 0], + ....: [1, 0, 1, 0, 0, 0, 0, 0, 0], + ....: [0, 1, 0, 1, 0, 0, 0, 0, 0], + ....: [0, 0, 0, 0,-1, 1, 0, 1, 0], + ....: [0, 0, 0, 0,-1, 1, 0, 0, 1], + ....: [0, 0, 0, 0,-1, 0, 1, 1, 0], + ....: [0, 0, 0, 0,-1, 0, 1, 0, 1]]) + sage: result, certificate = M.is_totally_unimodular( + ....: certificate=True) + sage: result, certificate + (True, OneSumNode (9×9) with 2 children) + sage: unicode_art(certificate) + ╭───────────OneSumNode (9×9) with 2 children + │ │ + GraphicNode (5×4) CographicNode (4×5) + sage: result, certificate = M.is_totally_unimodular( + ....: certificate=True, stop_when_nonTU=False) + sage: result, certificate + (True, OneSumNode (9×9) with 2 children) + sage: unicode_art(certificate) + ╭───────────OneSumNode (9×9) with 2 children + │ │ + GraphicNode (5×4) CographicNode (4×5) + + This is test ``TreeFlagsNorecurse``, ``TreeFlagsStopNoncographic``, + and ``TreeFlagsStopNongraphic`` in CMR's ``test_regular.cpp``, + the underlying binary linear matroid is regular, + but the matrix is not totally unimodular:: + + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 9, 9, sparse=True), + ....: [[1, 1, 0, 0, 0, 0, 0, 0, 0], + ....: [1, 1, 1, 0, 0, 0, 0, 0, 0], + ....: [1, 0, 0, 1, 0, 0, 0, 0, 0], + ....: [0, 1, 1, 1, 0, 0, 0, 0, 0], + ....: [0, 0, 1, 1, 0, 0, 0, 0, 0], + ....: [0, 0, 0, 0, 1, 1, 1, 0, 0], + ....: [0, 0, 0, 0, 1, 1, 0, 1, 0], + ....: [0, 0, 0, 0, 0, 1, 0, 1, 1], + ....: [0, 0, 0, 0, 0, 0, 1, 1, 1]]) + sage: result, certificate = M.is_totally_unimodular( + ....: certificate=True) + sage: result, certificate + (False, (OneSumNode (9×9) with 2 children, ((3, 2, 0), (3, 1, 0)))) + sage: unicode_art(certificate[0]) + ╭OneSumNode (9×9) with 2 children─╮ + │ │ + ThreeConnectedIrregularNode (5×4) UnknownNode (4×5) + sage: result, certificate = M.is_totally_unimodular( + ....: certificate=True, + ....: stop_when_nonTU=False) + sage: result, certificate + (False, (OneSumNode (9×9) with 2 children, ((3, 2, 0), (3, 1, 0)))) + sage: unicode_art(certificate[0]) + ╭OneSumNode (9×9) with 2 children─╮ + │ │ + ThreeConnectedIrregularNode (5×4) ThreeConnectedIrregularNode (4×5) + """ + base_ring = self.parent().base_ring() + if base_ring.characteristic() not in [0, 3] : + raise ValueError(f'only defined over characteristic 0 or 3, got {base_ring}') + + cdef bool result_bool + cdef CMR_TU_PARAMS params + cdef CMR_TU_STATS stats + cdef CMR_SEYMOUR_NODE *dec = NULL + cdef CMR_SUBMAT *submat = NULL + + cdef CMR_SEYMOUR_NODE **pdec = &dec + cdef CMR_SUBMAT **psubmat = &submat + + if three_sum_pivot_children: + raise NotImplementedError + cdef dict kwds = dict(use_direct_graphicness_test=use_direct_graphicness_test, + prefer_graphicness=prefer_graphicness, + series_parallel_ok=series_parallel_ok, + check_graphic_minors_planar=check_graphic_minors_planar, + stop_when_irregular=stop_when_nonTU, + stop_when_nongraphic=False, + stop_when_noncographic=False, + stop_when_nongraphic_and_noncographic=False, + three_sum_pivot_children=three_sum_pivot_children, + three_sum_strategy=three_sum_strategy, + construct_leaf_graphs=construct_leaf_graphs, + construct_all_graphs=construct_all_graphs) + + params.algorithm = CMR_TU_ALGORITHM_DECOMPOSITION + params.ternary = True + params.camionFirst = False + _set_cmr_seymour_parameters(¶ms.seymour, kwds) + sig_on() + try: + CMR_CALL(CMRtuTest(cmr, self._mat, &result_bool, pdec, psubmat, + ¶ms, &stats, time_limit)) + finally: + sig_off() + + result = result_bool + if not certificate: + return result + node = create_DecompositionNode(dec, self, row_keys, column_keys, base_ring=ZZ) + + if result: + return result, node + + if submat == NULL: + submat_tuple = None + else: + submat_tuple = (tuple(submat.rows[i] for i in range(submat.numRows)), + tuple(submat.columns[i] for i in range(submat.numColumns))) + + return result, (node, submat_tuple) + + def is_complement_totally_unimodular(self, *, time_limit=60.0, certificate=False, + use_direct_graphicness_test=True, + series_parallel_ok=True, + check_graphic_minors_planar=False, + complete_tree='find_irregular', + construct_matrices=False, + construct_transposes=False, + construct_graphs=False, + row_keys=None, + column_keys=None): + raise NotImplementedError + + +cdef _set_cmr_seymour_parameters(CMR_SEYMOUR_PARAMS *params, dict kwds): + """ + Set the parameters for Seymour's decomposition from the dictionary ``kwds``. + + INPUT: + + - ``params`` -- the parameters object to be set + + Keyword arguments: + + - ``stop_when_irregular`` -- boolean; + whether to stop decomposing once irregularity is determined. + + - ``stop_when_nongraphic`` -- boolean; + whether to stop decomposing once non-graphicness (or being non-network) is determined. + + - ``stop_when_noncographic`` -- boolean; + whether to stop decomposing once non-cographicness (or being non-conetwork) is determined. + + - ``stop_when_nongraphic_and_noncographic`` -- boolean; + whether to stop decomposing once non-graphicness and non-cographicness + (or not being network and not being conetwork) is determined. + + - ``series_parallel_ok`` -- boolean (default: ``True``); + whether to allow series-parallel operations in the decomposition tree. + + - ``check_graphic_minors_planar`` -- boolean (default: ``False``); + whether minors identified as graphic should still be checked for cographicness. + + - ``use_direct_graphicness_test`` -- boolean (default: ``True``); + whether to use fast graphicness routines. + + - ``prefer_graphicness`` -- boolean; + whether to first test for (co)graphicness (or being (co)network) + before applying series-parallel reductions. + + - ``three_sum_pivot_children`` -- boolean; + whether pivots for 3-sums shall be applied such that the matrix contains + both child matrices as submatrices, if possible. + + - ``three_sum_strategy`` -- ``"Mixed_Mixed"`` or ``"Wide_Wide"`` or integer; + whether to perform pivots to change the rank distribution, and how to construct the children. + + The value is a bit-wise "or" of three decisions. + + The first decision is that of the rank distribution: + - CMR_SEYMOUR_THREESUM_FLAG_NO_PIVOTS to not change the rank distribution (default), or + - CMR_SEYMOUR_THREESUM_FLAG_DISTRIBUTED_RANKS to enforce distributed ranks (1 + 1), or + - CMR_SEYMOUR_THREESUM_FLAG_CONCENTRATED_RANK to enforce concentrated ranks (2 + 0). + + The second decision determines the layout of the first child matrix: + - CMR_SEYMOUR_THREESUM_FLAG_FIRST_WIDE for a wide first child (default) + in case of distributed ranks, or + - CMR_SEYMOUR_THREESUM_FLAG_FIRST_TALL for a tall first child in that case. + - CMR_SEYMOUR_THREESUM_FLAG_FIRST_MIXED for a mixed first child (default) + in case of concentrated ranks, or + - CMR_SEYMOUR_THREESUM_FLAG_FIRST_ALLREPR for a first child + with all representing rows in that case. + + Similarly, the third decision determines the layout of the second child matrix: + - CMR_SEYMOUR_THREESUM_FLAG_SECOND_WIDE for a wide second child (default) + in case of distributed ranks, or + - CMR_SEYMOUR_THREESUM_FLAG_SECOND_TALL for a tall second child in that case. + - CMR_SEYMOUR_THREESUM_FLAG_SECOND_MIXED for a mixed second child (default) + in case of concentrated ranks, or + - CMR_SEYMOUR_THREESUM_FLAG_SECOND_ALLREPR for a first second + with all representing rows in that case. + + .. SEEALSO:: :meth:`three_sum_wide_wide`, :meth:`three_sum_mixed_mixed` + + .. NOTE:: + + A decomposition as described by Seymour can be selected via CMR_SEYMOUR_THREESUM_FLAG_SEYMOUR. + A decomposition as used by Truemper can be selected via CMR_SEYMOUR_THREESUM_FLAG_TRUEMPER. + + The default (``None``) is to not carry out any pivots and + choose Seymour's or Truemper's definition depending on the rank distribution. + + ``"Mixed_Mixed"`` is to allow pivots and choose CMR_SEYMOUR_THREESUM_FLAG_TRUEMPER + + ``"Wide_Wide"`` is to allow pivots and choose CMR_SEYMOUR_THREESUM_FLAG_SEYMOUR + + - ``construct_leaf_graphs`` -- boolean; + whether to construct (co)graphs for all leaf nodes that are (co)graphic or (co)network. + + - ``construct_all_graphs`` -- boolean; + whether to construct (co)graphs for all nodes that are (co)graphic or (co)network. + """ + CMR_CALL(CMRseymourParamsInit(params)) + params.stopWhenIrregular = kwds['stop_when_irregular'] + params.stopWhenNongraphic = kwds['stop_when_nongraphic'] + params.stopWhenNoncographic = kwds['stop_when_noncographic'] + params.stopWhenNeitherGraphicNorCoGraphic = kwds['stop_when_nongraphic_and_noncographic'] + params.directGraphicness = kwds['use_direct_graphicness_test'] + params.preferGraphicness = kwds['prefer_graphicness'] + params.seriesParallel = kwds['series_parallel_ok'] + params.planarityCheck = kwds['check_graphic_minors_planar'] + params.threeSumPivotChildren = kwds['three_sum_pivot_children'] + if kwds['three_sum_strategy'] is not None: + if kwds['three_sum_strategy'] == 'Mixed_Mixed': + params.threeSumStrategy = CMR_SEYMOUR_THREESUM_FLAG_CONCENTRATED_RANK | CMR_SEYMOUR_THREESUM_FLAG_FIRST_MIXED | CMR_SEYMOUR_THREESUM_FLAG_SECOND_MIXED + elif kwds['three_sum_strategy'] == 'Wide_Wide': + params.threeSumStrategy = CMR_SEYMOUR_THREESUM_FLAG_DISTRIBUTED_RANKS | CMR_SEYMOUR_THREESUM_FLAG_FIRST_WIDE | CMR_SEYMOUR_THREESUM_FLAG_SECOND_WIDE + else: + params.threeSumStrategy = kwds['three_sum_strategy'] + params.constructLeafGraphs = kwds['construct_leaf_graphs'] + params.constructAllGraphs = kwds['construct_all_graphs'] + + +cdef _sage_edge(CMR_GRAPH *graph, CMR_GRAPH_EDGE e): + return Integer(CMRgraphEdgeU(graph, e)), Integer(CMRgraphEdgeV(graph, e)) + + +cdef _sage_edges(CMR_GRAPH *graph, CMR_GRAPH_EDGE *edges, int n, keys): + if keys is None: + return tuple(_sage_edge(graph, edges[i]) + for i in range(n)) + return {key: _sage_edge(graph, edges[i]) + for i, key in enumerate(keys)} + + +cdef _sage_graph(CMR_GRAPH *graph): + # + + # The indices of the vertices have no meaning. + # TODO: Can we label them canonically based on the edges? + + # Until we have a proper CMR Graph backend, we just create a Sage graph with whatever backend + from sage.graphs.graph import Graph + + def vertices(): + i = CMRgraphNodesFirst(graph) + while CMRgraphNodesValid(graph, i): + yield i + i = CMRgraphNodesNext(graph, i) + + def edges(): + i = CMRgraphEdgesFirst(graph) + while CMRgraphEdgesValid(graph, i): + e = CMRgraphEdgesEdge(graph, i) + yield _sage_edge(graph, e) + i = CMRgraphEdgesNext(graph, i) + + return Graph([list(vertices()), list(edges())]) + + +cdef _sage_arc(CMR_GRAPH *graph, CMR_GRAPH_EDGE e, bint reversed): + if reversed: + return Integer(CMRgraphEdgeV(graph, e)), Integer(CMRgraphEdgeU(graph, e)) + return Integer(CMRgraphEdgeU(graph, e)), Integer(CMRgraphEdgeV(graph, e)) + + +cdef _sage_arcs(CMR_GRAPH *graph, CMR_GRAPH_EDGE *arcs, bool *arcs_reversed, n, keys): + if keys is None: + return tuple(_sage_arc(graph, arcs[i], arcs_reversed[arcs[i]]) + for i in range(n)) + return {key: _sage_arc(graph, arcs[i], arcs_reversed[arcs[i]]) + for i, key in enumerate(keys)} + + +cdef _sage_digraph(CMR_GRAPH *graph, bool *arcs_reversed): + from sage.graphs.digraph import DiGraph + + def vertices(): + i = CMRgraphNodesFirst(graph) + while CMRgraphNodesValid(graph, i): + yield i + i = CMRgraphNodesNext(graph, i) + + def arcs(): + i = CMRgraphEdgesFirst(graph) + while CMRgraphEdgesValid(graph, i): + e = CMRgraphEdgesEdge(graph, i) + yield _sage_arc(graph, e, arcs_reversed[e]) + i = CMRgraphEdgesNext(graph, i) + + return DiGraph([list(vertices()), list(arcs())]) diff --git a/src/sage/matrix/matrix_integer_dense.pyx b/src/sage/matrix/matrix_integer_dense.pyx index b6a832c38d9..7bd43b89a6b 100644 --- a/src/sage/matrix/matrix_integer_dense.pyx +++ b/src/sage/matrix/matrix_integer_dense.pyx @@ -3297,6 +3297,28 @@ cdef class Matrix_integer_dense(Matrix_dense): else: return R + def is_unimodular(self): + r""" + EXAMPLES:: + + sage: M = matrix(ZZ, [[1, 0, 0], [0, 1, 0]]); M + [1 0 0] + [0 1 0] + sage: M.is_unimodular() + True + sage: M = matrix(ZZ, [[1, 1, 0], [-1, 1, 1]]); M + [ 1 1 0] + [-1 1 1] + sage: M.is_unimodular() + False + """ + from .matrix_cmr_sparse import Matrix_cmr_chr_sparse + from .matrix_space import MatrixSpace + + MS = MatrixSpace(ZZ, self.nrows(), self.ncols(), sparse=True) + M = Matrix_cmr_chr_sparse(MS, self) + return M.is_unimodular() + def is_LLL_reduced(self, delta=None, eta=None): r""" Return ``True`` if this lattice is `(\delta, \eta)`-LLL reduced. diff --git a/src/sage/matrix/seymour_decomposition.pxd b/src/sage/matrix/seymour_decomposition.pxd new file mode 100644 index 00000000000..6d4018628e8 --- /dev/null +++ b/src/sage/matrix/seymour_decomposition.pxd @@ -0,0 +1,51 @@ +# sage_setup: distribution = sagemath-cmr +from sage.libs.cmr.cmr cimport CMR_SEYMOUR_NODE, CMR_ELEMENT +from sage.structure.sage_object cimport SageObject + + +cdef class DecompositionNode(SageObject): + cdef object _base_ring + cdef object _matrix + cdef CMR_SEYMOUR_NODE *_dec + cdef object _row_keys + cdef object _column_keys + cdef object _child_nodes + cdef object _minors + + cdef _set_dec(self, CMR_SEYMOUR_NODE *dec) + cdef _set_root_dec(self) + cdef _set_row_keys(self, row_keys) + cdef _set_column_keys(self, column_keys) + + cdef _CMRelement_to_key(self, CMR_ELEMENT element) + + +cdef class BaseGraphicNode(DecompositionNode): + cdef object _graph + cdef object _forest_edges + cdef object _coforest_edges + + +cdef class GraphicNode(BaseGraphicNode): + pass + + +cdef class CographicNode(BaseGraphicNode): + pass + + +cdef class PlanarNode(BaseGraphicNode): + cdef object _cograph + cdef object _cograph_forest_edges + cdef object _cograph_coforest_edges + + +cdef class SymbolicNode(DecompositionNode): + + cdef object _symbol + + +cdef create_DecompositionNode(CMR_SEYMOUR_NODE *dec, + matrix=?, + row_keys=?, column_keys=?, + base_ring=?) diff --git a/src/sage/matrix/seymour_decomposition.pyx b/src/sage/matrix/seymour_decomposition.pyx new file mode 100644 index 00000000000..40ffe6d5a90 --- /dev/null +++ b/src/sage/matrix/seymour_decomposition.pyx @@ -0,0 +1,3872 @@ +# sage_setup: distribution = sagemath-cmr +# sage.doctest: optional - sage.libs.cmr +r""" +Seymour's decomposition of totally unimodular matrices and regular matroids + +This module is provided by the distribution :ref:`sagemath-cmr `. +""" + +# **************************************************************************** +# Copyright (C) 2023 Javier Santillan +# 2023-2024 Matthias Koeppe +# 2023-2024 Luze Xu +# +# 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/ +# **************************************************************************** + +from libc.stdint cimport SIZE_MAX + +from cysignals.signals cimport sig_on, sig_off + +from sage.libs.cmr.cmr cimport * +from sage.misc.cachefunc import cached_method +from sage.rings.integer cimport Integer +from sage.rings.integer_ring import ZZ +from sage.structure.sage_object cimport SageObject + +from .constructor import Matrix +from .matrix_cmr_sparse cimport Matrix_cmr_chr_sparse, _sage_edges, _sage_graph, _set_cmr_seymour_parameters +from .matrix_cmr_sparse cimport _sage_arcs, _sage_digraph +from .matrix_space import MatrixSpace + + +cdef class DecompositionNode(SageObject): + r""" + Base class for nodes in Seymour's decomposition + """ + + def __cinit__(self, *args, **kwds): + r""" + Initialize the internal decomposition, a ``CMR_SEYMOUR_NODE``. + """ + self._dec = NULL + + def __init__(self, matrix=None, row_keys=None, column_keys=None, base_ring=None): + r""" + Create a node in Seymour's decomposition. + + INPUT: + + - ``matrix`` -- the internal matrix representing the node. + Convert to a :class:`Matrix_cmr_chr_sparse`. + + - ``row_keys`` -- a finite or enumerated family of arbitrary objects + that index the rows of the matrix + + - ``column_keys`` -- a finite or enumerated family of arbitrary objects + that index the columns of the matrix + + - ``base_ring`` -- the base ring of ``matrix`` representing the node. + For Seymour decomposition node, the base ring is `\GF{2}` or `\GF{3}` or ``ZZ``. + If the base ring is `\GF{2}`, the node and the matrix deal with + the underlying binary linear matroid; + if the base ring is `\GF(3)` or ``ZZ``, the node deals with + the matrix decomposition. + + A :class:`DecompositionNode` is usually created with an internal decomposition, + ``self._dec``, see :meth:create_DecompositionNode + Such decomposition comes from the certificate of the + totally unimodularity test, see + :meth:`matrix_cmr_sparse.Matrix_cmr_chr_sparse.is_totally_unimodular` + + Another usage is to create a :class:`UnknownNode` from a matrix. + A root dummy decomposition is created before completing + the decomposition, see :meth:_set_root_dec, :meth:complete_decomposition + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 2, sparse=True), + ....: [[1, 0], [-1, 1], [0, 1]]); M + [ 1 0] + [-1 1] + [ 0 1] + sage: result, certificate = M.is_totally_unimodular(certificate=True) + sage: result, certificate + (True, GraphicNode (3×2)) + + sage: from sage.matrix.seymour_decomposition import UnknownNode + sage: node = UnknownNode([[1, 1], [0, 1]]); node + UnknownNode (2×2) + """ + if matrix is None: + self._matrix = None + elif isinstance(matrix, Matrix_cmr_chr_sparse): + self._matrix = matrix + else: + try: + self._matrix = matrix._matrix_cmr() + except (AttributeError, ImportError, TypeError): + if base_ring is not None: + matrix = Matrix(matrix, ring=base_ring) + else: + matrix = Matrix(matrix) + self._matrix = Matrix_cmr_chr_sparse(matrix.parent(), matrix) + else: + if row_keys is None: + row_keys = matrix.codomain().basis().keys() + if column_keys is None: + column_keys = matrix.domain().basis().keys() + if row_keys is not None: + self._set_row_keys(row_keys) + if column_keys is not None: + self._set_column_keys(column_keys) + if base_ring is None: + if self._matrix is not None: + base_ring = self._matrix.parent().base_ring() + self._base_ring = base_ring + + cdef _set_dec(self, CMR_SEYMOUR_NODE *dec): + r""" + Set the decomposition ``self._dec`` to ``dec``. + If the value was previously set, then free it first. + """ + if self._dec != NULL: + # We own it, so we have to free it. + CMR_CALL(CMRseymourRelease(cmr, &self._dec)) + if dec != NULL: + CMR_CALL(CMRseymourCapture(cmr, dec)) + self._dec = dec + + cdef _set_root_dec(self): + r""" + Set the decomposition by creating a root ``CMR_SEYMOUR_NODE`` + based on the internal matrix representation ``self._matrix``. + """ + cdef CMR_SEYMOUR_NODE *root + cdef Matrix_cmr_chr_sparse matrix + try: + matrix = self.matrix() + except Exception: + raise ValueError('no Matrix_cmr_chr_sparse matrix') + base_ring = self.base_ring() + if base_ring.characteristic() not in [0, 2, 3] : + raise ValueError(f'only defined over binary or ternary, got {base_ring}') + isTernary = base_ring.characteristic() != 2 + cdef CMR_CHRMAT *mat = matrix._mat + + sig_on() + try: + CMR_CALL(CMRseymourCreate(cmr, &root, isTernary, mat)) + finally: + sig_off() + self._set_dec(root) + + cdef _set_row_keys(self, row_keys): + """ + Set the row keys with consistency checking: if the + value was previously set, it must remain the same. + """ + if row_keys is not None: + row_keys = tuple(row_keys) + if self._row_keys is not None and self._row_keys != row_keys: + raise ValueError(f"inconsistent row keys: should be {self._row_keys} " + f"but got {row_keys}") + if row_keys is not None and self._dec != NULL and self.nrows() != len(row_keys): + raise ValueError(f"inconsistent row keys: should be of cardinality {self.nrows()} " + f"but got {row_keys}") + self._row_keys = row_keys + + cdef _set_column_keys(self, column_keys): + """ + Set the column keys with consistency checking: if the + value was previously set, it must remain the same. + """ + if column_keys is not None: + column_keys = tuple(column_keys) + if self._column_keys is not None and self._column_keys != column_keys: + raise ValueError(f"inconsistent column keys: should be {self._column_keys} " + f"but got {column_keys}") + if column_keys is not None and self._dec != NULL and self.ncols() != len(column_keys): + raise ValueError(f"inconsistent column keys: should be of cardinality {self.ncols()} " + f"but got {column_keys}") + self._column_keys = column_keys + + def __dealloc__(self): + """ + Frees all the memory allocated for this node. + """ + self._set_dec(NULL) + + def __hash__(self): + """ + Return a hash of this node. It is the hash of the decomposition. + """ + return self._dec + + def nrows(self): + r""" + Return the number of rows of the internal matrix representing this node. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 2, sparse=True), + ....: [[1, 0], [-1, 1], [0, 1]]); M + [ 1 0] + [-1 1] + [ 0 1] + sage: result, certificate = M.is_totally_unimodular(certificate=True) + sage: result, certificate + (True, GraphicNode (3×2)) + sage: certificate.nrows() + 3 + sage: certificate.ncols() + 2 + + sage: from sage.matrix.seymour_decomposition import UnknownNode + sage: node = UnknownNode([[1, 1], [0, 1]]); node + UnknownNode (2×2) + sage: node.nrows() + 2 + sage: node.ncols() + 2 + """ + if self._row_keys is not None: + return len(self._row_keys) + if self._dec != NULL: + return CMRseymourNumRows(self._dec) + if self._matrix is not None: + return self._matrix.nrows() + raise RuntimeError('nrows undefined') + + def ncols(self): + r""" + Return the number of columns of the internal matrix representing this node. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 2, sparse=True), + ....: [[1, 0], [-1, 1], [0, 1]]); M + [ 1 0] + [-1 1] + [ 0 1] + sage: result, certificate = M.is_totally_unimodular(certificate=True) + sage: result, certificate + (True, GraphicNode (3×2)) + sage: certificate.nrows() + 3 + sage: certificate.ncols() + 2 + + sage: from sage.matrix.seymour_decomposition import UnknownNode + sage: node = UnknownNode([[1, 1], [0, 1]]); node + UnknownNode (2×2) + sage: node.nrows() + 2 + sage: node.ncols() + 2 + """ + if self._column_keys is not None: + return len(self._column_keys) + if self._dec != NULL: + return CMRseymourNumColumns(self._dec) + if self._matrix is not None: + return self._matrix.ncols() + raise RuntimeError('ncols undefined') + + def dimensions(self): + r""" + Return the number of rows and columns of this node. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 2, sparse=True), + ....: [[1, 0], [-1, 1], [0, 1]]); M + [ 1 0] + [-1 1] + [ 0 1] + sage: result, certificate = M.is_totally_unimodular(certificate=True) + sage: result, certificate + (True, GraphicNode (3×2)) + sage: certificate.dimensions() + (3, 2) + + sage: from sage.matrix.seymour_decomposition import UnknownNode + sage: node = UnknownNode([[1, 1], [0, 1]]); node + UnknownNode (2×2) + sage: node.dimensions() + (2, 2) + """ + return self.nrows(), self.ncols() + + def base_ring(self): + r""" + Return the base ring of the matrix representing the node. + """ + return self._base_ring + + def matrix(self): + r""" + Return a :class:`Matrix`. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 2, sparse=True), + ....: [[1, 0], [-1, 1], [0, 1]]); M + [ 1 0] + [-1 1] + [ 0 1] + sage: result, certificate = M.is_totally_unimodular(certificate=True) + sage: result, certificate + (True, GraphicNode (3×2)) + sage: certificate.matrix() + [ 1 0] + [-1 1] + [ 0 1] + + sage: from sage.matrix.seymour_decomposition import UnknownNode + sage: node = UnknownNode([[1, 1], [0, 1]]); node + UnknownNode (2×2) + sage: node.matrix() + [1 1] + [0 1] + """ + if self._matrix is not None: + return self._matrix + if self._dec is NULL: + if isinstance(self, SumNode): + return self.block_matrix_form() + raise ValueError('Matrix and decomposition are both missing') + cdef Matrix_cmr_chr_sparse result + cdef CMR_CHRMAT *mat = CMRseymourGetMatrix(self._dec) + if mat == NULL: + return None + ms = MatrixSpace(self.base_ring(), mat.numRows, mat.numColumns, sparse=True) + result = Matrix_cmr_chr_sparse.__new__(Matrix_cmr_chr_sparse, ms) + result._mat = mat + result._root = self # Matrix is owned by us + self._matrix = result + return result + + def row_keys(self): + r""" + Return the row keys of this node. + + OUTPUT: a tuple or ``None`` + + EXAMPLES:: + + sage: from sage.matrix.seymour_decomposition import UnknownNode + sage: node = UnknownNode(matrix(ZZ, [[1, 0, 1], [0, 1, 1]]), + ....: row_keys='ab', + ....: column_keys=range(3)); node + UnknownNode (2×3) + sage: node.row_keys() + ('a', 'b') + """ + return self._row_keys + + def column_keys(self): + r""" + Return the column keys of this node. + + OUTPUT: a tuple or ``None`` + + EXAMPLES:: + + sage: from sage.matrix.seymour_decomposition import UnknownNode + sage: node = UnknownNode(matrix(ZZ, [[1, 0, 1], [0, 1, 1]]), + ....: row_keys='ab', + ....: column_keys=range(3)); node + UnknownNode (2×3) + sage: node.column_keys() + (0, 1, 2) + """ + return self._column_keys + + def set_default_keys(self): + r""" + Set default row and column keys. + + .. SEEALSO:: :class:`ElementKey` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 2, sparse=True), + ....: [[1, 0], [-1, 1], [0, 1]]); M + [ 1 0] + [-1 1] + [ 0 1] + sage: result, certificate = M.is_totally_unimodular(certificate=True) + sage: result, certificate + (True, GraphicNode (3×2)) + sage: certificate.row_keys() is None + True + sage: certificate.set_default_keys() + sage: certificate.row_keys() + (r0, r1, r2) + """ + row_keys = self.row_keys() + column_keys = self.column_keys() + if row_keys is None or column_keys is None: + row_keys = tuple(ElementKey(f"r{i}") for i in range(self.nrows())) + column_keys = tuple(ElementKey(f"c{i}") for i in range(self.ncols())) + elif not isinstance(row_keys[0], ElementKey): + row_keys = tuple(ElementKey(key) for key in row_keys) + column_keys = tuple(ElementKey(key) for key in column_keys) + self._row_keys = row_keys + self._column_keys = column_keys + + @cached_method + def morphism(self): + r""" + Create the matrix in the distinguished bases of the domain and codomain + to build the module morphism. + + See also: :class:`sage.modules.with_basis.morphism.ModuleMorphismFromMatrix`. + + EXAMPLES:: + + sage: from sage.matrix.seymour_decomposition import UnknownNode + sage: node = UnknownNode(matrix(ZZ, [[1, 0, 1], [0, 1, 1]]), + ....: row_keys='ab', + ....: column_keys=range(3)); node + UnknownNode (2×3) + sage: node.matrix() + [1 0 1] + [0 1 1] + sage: node.morphism()._unicode_art_matrix() + 0 1 2 + a⎛1 0 1⎞ + b⎝0 1 1⎠ + """ + return Matrix(self.matrix(), + row_keys=self.row_keys(), + column_keys=self.column_keys()) + + def as_ordered_tree(self): + r""" + Return the decomposition tree rooted at ``self``. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = matrix([[1, 0], [-1, 1], [0, 1]], sparse=True) + sage: M2 = block_diagonal_matrix([M, M], sparse=True) + sage: M2cmr = Matrix_cmr_chr_sparse(M2.parent(), M2); M2cmr + [ 1 0 0 0] + [-1 1 0 0] + [ 0 1 0 0] + [ 0 0 1 0] + [ 0 0 -1 1] + [ 0 0 0 1] + sage: result, certificate = M2cmr.is_totally_unimodular(certificate=True) + sage: T = certificate.as_ordered_tree(); T + OneSumNode (6×4) with 2 children[GraphicNode (3×2)[], GraphicNode (3×2)[]] + sage: unicode_art(T) + ╭───────────OneSumNode (6×4) with 2 children + │ │ + GraphicNode (3×2) GraphicNode (3×2) + """ + from sage.combinat.ordered_tree import LabelledOrderedTree + return LabelledOrderedTree([child.as_ordered_tree() for child in self.child_nodes()], + label=self) + + def plot(self, **kwds): + r""" + Plot the decomposition tree rooted at ``self``. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = matrix([[1, 0], [-1, 1], [0, 1]], sparse=True) + sage: M2MT = block_diagonal_matrix([M, M, M.T], sparse=True) + sage: M2MTcmr = Matrix_cmr_chr_sparse(M2MT.parent(), M2MT) + sage: result, certificate = M2MTcmr.is_totally_unimodular(certificate=True) + sage: T = certificate.as_ordered_tree() + sage: T.plot() # needs sage.plot + Graphics object consisting of 8 graphics primitives + """ + return self.as_ordered_tree().plot(**kwds) + + def is_ternary(self): + r""" + Return whether the decomposition is over `\GF{3}`. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 2, sparse=True), + ....: [[1, 0], [-1, 1], [0, 1]]); M + [ 1 0] + [-1 1] + [ 0 1] + sage: result, certificate = M.is_totally_unimodular(certificate=True) + sage: result, certificate + (True, GraphicNode (3×2)) + sage: certificate.is_ternary() + True + + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 2, sparse=True), + ....: [[1, 0], [1, 1], [0, 1]]); M + [1 0] + [1 1] + [0 1] + sage: result, certificate = M._is_binary_linear_matroid_regular(certificate=True) + sage: result, certificate + (True, GraphicNode (3×2)) + sage: certificate.is_ternary() + False + """ + return CMRseymourIsTernary(self._dec) + + def nchildren(self): + r""" + Return the number of children of the node. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 2, sparse=True), + ....: [[1, 0], [-1, 1], [0, 1]]); M + [ 1 0] + [-1 1] + [ 0 1] + sage: result, certificate = M.is_totally_unimodular(certificate=True) + sage: result, certificate + (True, GraphicNode (3×2)) + sage: certificate.nchildren() + 0 + """ + if self._child_nodes is not None: + return len(self._child_nodes) + if self._dec == NULL: + return 0 + return CMRseymourNumChildren(self._dec) + + cdef _CMRelement_to_key(self, CMR_ELEMENT element): + r""" + Transform a ``CMRelement`` (row or column index implemented in cmr) + to a row key or a column key. + """ + if not CMRelementIsValid(element): + raise ValueError('CMRelement index not valid. Extra row or column is detected.') + if self.row_keys() is None or self.column_keys() is None: + raise ValueError('row_keys and column_keys are required') + if CMRelementIsRow(element): + return self.row_keys()[CMRelementToRowIndex(element)] + else: + return self.column_keys()[CMRelementToColumnIndex(element)] + + def _create_child_node(self, index): + r""" + Return the child node of ``self`` corresponding to the ``index``, + and the corresponding row and column keys in the parent node. + + OUTPUT: a tuple of (child node, child row keys, child column keys) + """ + row_keys = self.row_keys() + column_keys = self.column_keys() + cdef CMR_SEYMOUR_NODE *child_dec = CMRseymourChild(self._dec, index) + cdef CMR_ELEMENT *parent_rows = CMRseymourChildRowsToParent(self._dec, index) + cdef CMR_ELEMENT *parent_columns = CMRseymourChildColumnsToParent(self._dec, index) + child_nrows = CMRseymourNumRows(child_dec) + child_ncols = CMRseymourNumColumns(child_dec) + + if parent_rows == NULL or all(parent_rows[i] == 0 for i in range(child_nrows)): + raise ValueError(f"Child {index} does not have parents rows") + parent_rows_tuple = tuple(parent_rows[i] for i in range(child_nrows)) + + if parent_columns == NULL or all(parent_columns[i] == 0 for i in range(child_ncols)): + raise ValueError(f"Child {index} does not have parents columns") + parent_columns_tuple = tuple(parent_columns[i] for i in range(child_ncols)) + + if row_keys is not None and column_keys is not None: + child_row_keys = tuple(self._CMRelement_to_key(element) + for element in parent_rows_tuple) + child_column_keys = tuple(self._CMRelement_to_key(element) + for element in parent_columns_tuple) + child = create_DecompositionNode(child_dec, matrix=None, + row_keys=child_row_keys, + column_keys=child_column_keys, + base_ring=self.base_ring()) + else: + child_row_keys = tuple(CMRelementToRowIndex(element) + for element in parent_rows_tuple) + child_column_keys = tuple(CMRelementToColumnIndex(element) + for element in parent_columns_tuple) + child = create_DecompositionNode(child_dec, matrix=None, + row_keys=child_row_keys, + column_keys=child_column_keys, + base_ring=self.base_ring()) + return child, child_row_keys, child_column_keys + + def _children(self): + r""" + Return a tuple of the tuples of children and their row and column keys. + The underlying implementation of :meth:`child_nodes` + and :meth:`child_indices`. + """ + if self._child_nodes is not None: + return self._child_nodes + children_tuple = tuple(self._create_child_node(index) + for index in range(self.nchildren())) + self._child_nodes = children_tuple + return self._child_nodes + + def child_nodes(self): + r""" + Return a tuple of the children. + + The children are sorted by the ordering inherited from cmr, which + is their appearance in the parent. + + In the case of :class:`SumNode`, this is the same as :meth:`~SumNode.summands`. + + For graphic or leaf nodes, it returns the empty tuple. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse.one_sum([[1, 0], [-1, 1]], + ....: [[1, 1], [-1, 0]], + ....: [[1, 0], [0,1]]); M + [ 1 0| 0 0| 0 0] + [-1 1| 0 0| 0 0] + [-----+-----+-----] + [ 0 0| 1 1| 0 0] + [ 0 0|-1 0| 0 0] + [-----+-----+-----] + [ 0 0| 0 0| 1 0] + [ 0 0| 0 0| 0 1] + sage: result, certificate = M.is_totally_unimodular(certificate=True); certificate + OneSumNode (6×6) with 4 children + sage: certificate.child_nodes() + (GraphicNode (2×2), GraphicNode (2×2), GraphicNode (1×1), GraphicNode (1×1)) + + sage: M2 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 2, 2, sparse=True), + ....: [[1, 1], [-1, 0]]); M2 + [ 1 1] + [-1 0] + sage: result, certificate = M2.is_totally_unimodular(certificate=True); certificate + GraphicNode (2×2) + sage: certificate.child_nodes() + () + """ + return tuple(child[0] for child in self._children()) + + def child_indices(self): + r""" + Return a tuple of the tuples of the row and column keys of children + in the parent node. + + OUTPUT: a tuple of (row keys, column keys) + + If the number of children is 1, then output (row keys, column keys). + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 2, sparse=True), + ....: [[1, 0], [-1, 1], [0, 1]]); M + [ 1 0] + [-1 1] + [ 0 1] + sage: result, certificate = M.is_totally_unimodular(certificate=True) + sage: certificate.child_indices() + () + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = matrix([[1, 0], [-1, 1], [0, 1]], sparse=True) + sage: M2 = block_diagonal_matrix([M, M], sparse=True) + sage: M2cmr = Matrix_cmr_chr_sparse(M2.parent(), M2); M2cmr + [ 1 0 0 0] + [-1 1 0 0] + [ 0 1 0 0] + [ 0 0 1 0] + [ 0 0 -1 1] + [ 0 0 0 1] + sage: result, certificate = M2cmr.is_totally_unimodular(certificate=True) + sage: result, certificate + (True, OneSumNode (6×4) with 2 children) + sage: C = certificate.summands(); C + (GraphicNode (3×2), GraphicNode (3×2)) + sage: certificate.child_indices()[0] + ((0, 1, 2), (0, 1)) + sage: certificate.child_indices()[1] + ((3, 4, 5), (2, 3)) + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: R12 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 9, 12, sparse=True), + ....: [[ 1, -1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1], + ....: [ 0, 0, 0, 1, -1, 0, 0, 0, 1, 1, 1, 1], + ....: [ 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1], + ....: [ 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0], + ....: [ 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, -1, -1], + ....: [ 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0], + ....: [ 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, -1, -1], + ....: [ 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0], + ....: [ 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1]]) + sage: result, certificate = R12.is_totally_unimodular(certificate=True, + ....: row_keys=['r1', 'r2', 'r3', 'r4', 'r5', + ....: 'r6', 'r7', 'r8', 'r9'], + ....: column_keys=['a','b','c','d','e','f', + ....: 'g','h','i','j','k','l']) + sage: C = certificate.child_nodes()[0]; C + ThreeSumNode (9×12) with 2 children + sage: certificate.child_indices() + ((r1, i, r3, r4, r5, r6, r7, r8, r9), (a, b, c, d, e, f, g, h, r2, j, k, l)) + + sage: M2 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 2, 2, sparse=True), + ....: [[1, 1], [-1, 0]]); M2 + [ 1 1] + [-1 0] + sage: result, certificate = M2.is_totally_unimodular(certificate=True); certificate + GraphicNode (2×2) + sage: certificate.child_indices() + () + """ + if self.nchildren() == 1: + child = self._children()[0] + return (child[1], child[2]) + return tuple((child[1], child[2]) for child in self._children()) + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: R12 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 6, 6, sparse=True), + ....: [[1,0,1,1,0,0], [0,1,1,1,0,0], [1,0,1,0,1,1], + ....: [0,-1,0,-1,1,1], [1,0,1,0,1,0], [0,-1,0,-1,0,1]]) + sage: result, certificate = R12.is_totally_unimodular(certificate=True, + ....: three_sum_strategy="Wide_Wide", + ....: row_keys=range(6), + ....: column_keys='abcdef') + sage: print(certificate) + PivotsNode (6×6) + """ + nrows, ncols = self.dimensions() + return f'{self.__class__.__name__} ({nrows}×{ncols})' + + def _unicode_art_(self): + r""" + Return a unicode art representation of the decomposition tree rooted at ``self``. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: R12 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 6, 6, sparse=True), + ....: [[1,0,1,1,0,0], [0,1,1,1,0,0], [1,0,1,0,1,1], + ....: [0,-1,0,-1,1,1], [1,0,1,0,1,0], [0,-1,0,-1,0,1]]) + sage: result, certificate = R12.is_totally_unimodular(certificate=True, + ....: three_sum_strategy="Wide_Wide", + ....: row_keys=range(6), + ....: column_keys='abcdef') + sage: unicode_art(certificate) + PivotsNode (6×6) + │ + ╭─────────────ThreeSumNode (6×6) with 2 children + │ │ + CographicNode (4×5) GraphicNode (4×5) + """ + return self.as_ordered_tree()._unicode_art_() + + def _ascii_art_(self): + r""" + Return a ascii art representation of the decomposition tree rooted at ``self``. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: R12 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 6, 6, sparse=True), + ....: [[1,0,1,1,0,0], [0,1,1,1,0,0], [1,0,1,0,1,1], + ....: [0,-1,0,-1,1,1], [1,0,1,0,1,0], [0,-1,0,-1,0,1]]) + sage: result, certificate = R12.is_totally_unimodular(certificate=True, + ....: three_sum_strategy="Wide_Wide", + ....: row_keys=range(6), + ....: column_keys='abcdef') + sage: ascii_art(certificate) + PivotsNode (6×6) + | + _________ThreeSumNode (6×6) with 2 children + / / + CographicNode (4×5) GraphicNode (4×5) + """ + return self.as_ordered_tree()._ascii_art_() + + def one_sum(*summands, **kwds): + r""" + Return a :class:`OneSumNode` constructed from the given nodes (summands). + + INPUT: + + - ``summands`` -- decomposition nodes :class:`DecompositionNode` + + - ``summand_ids`` -- a tuple or list of ids for summands + + - ``row_keys`` -- a finite or enumerated family of arbitrary objects + that index the rows of the result. + Must be consistent with the row keys of the summands + + - ``column_keys`` -- a finite or enumerated family of arbitrary objects + that index the columns of the result. + Must be consistent with the column keys of the summands + + Note that ``row_keys``, ``column_keys`` of ``summands`` are disjoint + + OUTPUT: A :class:`OneSumNode` + + The terminology "1-sum" is used in the context of Seymour's decomposition + of totally unimodular matrices and regular matroids, see [Sch1986]_. + + .. SEEALSO:: :meth:`two_sum`, :meth:`three_sum` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: from sage.matrix.seymour_decomposition import DecompositionNode + sage: M2 = Matrix_cmr_chr_sparse.one_sum([[1, 0], [-1, 1]], + ....: [[1, 1], [-1, 0]]) + sage: result, certificate = M2.is_totally_unimodular(certificate=True, + ....: row_keys=range(4), + ....: column_keys='abcd') + sage: certificate + OneSumNode (4×4) with 2 children + sage: certificate.summand_matrices() + ( + [ 1 0] [ 1 1] + [-1 1], [-1 0] + ) + sage: certificate.child_indices() + (((0, 1), ('a', 'b')), ((2, 3), ('c', 'd'))) + sage: node = DecompositionNode.one_sum(*certificate.child_nodes()) + sage: node.summand_matrices() + ( + [ 1 0] [ 1 1] + [-1 1], [-1 0] + ) + sage: node.child_indices() + (((0, 1), ('a', 'b')), ((2, 3), ('c', 'd'))) + + sage: M3 = Matrix_cmr_chr_sparse.one_sum([[1, 0], [-1, 1]], + ....: [[1]], [[-1]]) + sage: result, certificate = M3.is_totally_unimodular(certificate=True, + ....: row_keys=range(4), + ....: column_keys='abcd') + sage: certificate + OneSumNode (4×4) with 3 children + sage: certificate.summand_matrices() + ( + [ 1 0] + [-1 1], [1], [-1] + ) + sage: certificate.child_indices() + (((0, 1), ('a', 'b')), ((2,), ('c',)), ((3,), ('d',))) + sage: node = DecompositionNode.one_sum(*certificate.child_nodes()) + sage: node.summand_matrices() + ( + [ 1 0] + [-1 1], [1], [-1] + ) + sage: node.child_indices() + (((0, 1), ('a', 'b')), ((2,), ('c',)), ((3,), ('d',))) + + sage: from sage.matrix.seymour_decomposition import UnknownNode + sage: node = UnknownNode(matrix(ZZ, [[1, 0, 1], [0, 1, 1]]), + ....: row_keys='ab', + ....: column_keys=range(3)); node + UnknownNode (2×3) + sage: node = DecompositionNode.one_sum(certificate, node, summand_ids=range(2)) + sage: node.summand_matrices() + ( + [ 1 0| 0| 0] + [-1 1| 0| 0] + [-----+--+--] + [ 0 0| 1| 0] + [-----+--+--] [1 0 1] + [ 0 0| 0|-1], [0 1 1] + ) + sage: node.child_indices() + ((((0, 0), (0, 1), (0, 2), (0, 3)), ((0, 'a'), (0, 'b'), (0, 'c'), (0, 'd'))), + (((1, 'a'), (1, 'b')), ((1, 0), (1, 1), (1, 2)))) + + ``row_keys``, ``column_keys`` of ``summands`` are disjoint:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: from sage.matrix.seymour_decomposition import DecompositionNode + sage: M2 = Matrix_cmr_chr_sparse.one_sum([[1, 0], [-1, 1]], + ....: [[1, 1], [-1, 0]]) + sage: result, certificate = M2.is_totally_unimodular(certificate=True, + ....: row_keys='aefg', + ....: column_keys='abcd') + sage: node = DecompositionNode.one_sum(*certificate.child_nodes()) + Traceback (most recent call last): + ... + ValueError: keys must be disjoint, got + summand_row_keys=('a', 'e'), summand_column_keys=('a', 'b') + + sage: result, certificate = M2.is_totally_unimodular(certificate=True, + ....: row_keys=range(4), + ....: column_keys='abcd') + sage: node = DecompositionNode.one_sum(*certificate.child_nodes(), + ....: row_keys=range(4), + ....: column_keys='abce') + Traceback (most recent call last): + ... + ValueError: inconsistent column_keys, got column_keys=('a', 'b', 'c', 'e'), + should be a permutation of ['a', 'b', 'c', 'd'] + """ + summand_ids = kwds.pop('summand_ids', None) + row_keys = kwds.pop('row_keys', None) + column_keys = kwds.pop('column_keys', None) + if kwds: + raise ValueError(f'unknown keywords: {sorted(kwds)}') + + result = OneSumNode() + summands = tuple(summands) + if summand_ids is not None: + summand_ids = tuple(summand_ids) + else: + summand_ids = tuple(None for summand in summands) + # TODO: Make summands DecompositionNodes if not already + # Check row_keys, column_keys of summands are disjoint. Otherwise error + summands_row_keys = [] + summands_column_keys = [] + row_key_list = [] + column_key_list = [] + key_set = set() + for summand, id in zip(summands, summand_ids): + summand_row_keys = summand.row_keys() + summand_column_keys = summand.column_keys() + if id is not None: + summand_row_keys = tuple((id, key) for key in summand_row_keys) + summand_column_keys = tuple((id, key) for key in summand_column_keys) + + old_num_keys = len(key_set) + row_key_list.extend(summand_row_keys) + column_key_list.extend(summand_column_keys) + key_set.update(summand_row_keys) + key_set.update(summand_column_keys) + if old_num_keys + len(summand_row_keys) + len(summand_column_keys) != len(key_set): + raise ValueError(f'keys must be disjoint, ' + f'got {summand_row_keys=}, {summand_column_keys=}') + summands_row_keys.append(summand_row_keys) + summands_column_keys.append(summand_column_keys) + + if row_keys is not None: + row_keys = tuple(row_keys) + if set(row_keys) != set(row_key_list) or len(row_keys) != len(row_key_list): + raise ValueError(f'inconsistent row_keys, ' + f'got {row_keys=}, should be a permutation of {row_key_list}') + else: + row_keys = tuple(row_key_list) + if column_keys is not None: + column_keys = tuple(column_keys) + if set(column_keys) != set(column_key_list) or len(column_keys) != len(column_key_list): + raise ValueError(f'inconsistent column_keys, ' + f'got {column_keys=}, should be a permutation of {column_key_list}') + else: + column_keys = tuple(column_key_list) + + result._child_nodes = tuple(zip(summands, summands_row_keys, summands_column_keys)) + result._row_keys = row_keys + result._column_keys = column_keys + return result + + def _regularity(self): + r""" + Return whether the decomposition node is regular (binary) or TU (ternary). + If it is not determined, raise ValueError. + """ + cdef int8_t regularity + if self._dec != NULL: + regularity = CMRseymourRegularity(self._dec) + if regularity: + return regularity > 0 + raise ValueError('It is not determined whether the decomposition node is regular/TU') + + def _graphicness(self): + r""" + Return whether the decomposition node is graphic (binary) or network (ternary). + If it is not determined, raise ValueError. + """ + cdef int8_t graphicness + if self._dec != NULL: + graphicness = CMRseymourGraphicness(self._dec) + if graphicness: + return graphicness > 0 + raise ValueError('It is not determined whether the decomposition node is graphic/network') + + def _cographicness(self): + r""" + Return whether the decomposition node is cographic (binary) or conetwork (ternary). + If it is not determined, raise ValueError. + """ + cdef int8_t cographicness + if self._dec != NULL: + cographicness = CMRseymourCographicness(self._dec) + if cographicness: + return cographicness > 0 + raise ValueError('It is not determined whether the decomposition node is cographic/conetwork') + + def _is_binary_linear_matroid_graphic(self, *, decomposition=False, **kwds): + r""" + Return whether the linear matroid of ``self`` over `\GF{2}` is graphic. + If there is some entry not in `\{0, 1\}`, return ``False``. + + This method is based on Seymour's decomposition. + The decomposition will stop once nongraphicness is detected. + For direct graphicness check, + + .. SEEALSO:: + + - :meth:`sage.matrix.matrix_cmr_sparse.Matrix_cmr_chr_sparse._is_binary_linear_matroid_graphic` + - :meth:`UnknownNode._is_binary_linear_matroid_graphic` + - :meth:`_binary_linear_matroid_complete_decomposition` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: from sage.matrix.seymour_decomposition import DecompositionNode + sage: M2 = Matrix_cmr_chr_sparse.one_sum([[1, 0], [1, 1], [0, 1]], + ....: [[1, 1, 0], [0, 1, 1]]) + sage: result, certificate = M2.is_totally_unimodular(certificate=True, + ....: row_keys=range(5), + ....: column_keys='abcde') + sage: node = DecompositionNode.one_sum(*certificate.child_nodes()) + sage: node._graphicness() + Traceback (most recent call last): + ... + ValueError: It is not determined whether the decomposition node is graphic/network + sage: result, decomposition = node._is_binary_linear_matroid_graphic(decomposition=True) + sage: result + True + sage: unicode_art(decomposition) + ╭──────────OneSumNode (5×5) with 2 children + │ │ + PlanarNode (3×2) PlanarNode (2×3) + sage: decomposition._graphicness() + True + sage: node._is_binary_linear_matroid_graphic(certificate=True) + Traceback (most recent call last): + ... + NotImplementedError + + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(GF(2), 9, 9, sparse=True), + ....: [[1, 1, 0, 0, 0, 0, 0, 0, 0], + ....: [1, 1, 1, 0, 0, 0, 0, 0, 0], + ....: [1, 0, 0, 1, 0, 0, 0, 0, 0], + ....: [0, 1, 1, 1, 0, 0, 0, 0, 0], + ....: [0, 0, 1, 1, 0, 0, 0, 0, 0], + ....: [0, 0, 0, 0, 1, 1, 1, 0, 0], + ....: [0, 0, 0, 0, 1, 1, 0, 1, 0], + ....: [0, 0, 0, 0, 0, 1, 0, 1, 1], + ....: [0, 0, 0, 0, 0, 0, 1, 1, 1]]) + sage: result, certificate = M._is_binary_linear_matroid_regular(certificate=True, + ....: row_keys=[f'r{i}' for i in range(9)], + ....: column_keys=[f'c{i}' for i in range(9)]) + sage: node = DecompositionNode.one_sum(*certificate.child_nodes()) + sage: result, decomposition = node._is_binary_linear_matroid_graphic(decomposition=True) + sage: result + False + sage: unicode_art(decomposition) + ╭───────────OneSumNode (9×9) with 2 children + │ │ + GraphicNode (5×4) UnknownNode (4×5) + sage: decomposition.child_nodes()[1]._graphicness() + False + """ + certificate = kwds.get('certificate', False) + try: + result = self._graphicness() + if not decomposition and not certificate: + return result + result = [result] + if decomposition: + result.append(self) + if certificate: + raise NotImplementedError + return result + except ValueError: + # compute it... wait for CMR functions + full_dec = self._binary_linear_matroid_complete_decomposition( + stop_when_nongraphic=True, + check_graphic_minors_planar=True) + return full_dec._is_binary_linear_matroid_graphic( + decomposition=decomposition, + certificate=certificate) + + def _is_binary_linear_matroid_cographic(self, *, decomposition=False, **kwds): + r""" + Return whether the linear matroid of ``self`` over `\GF{2}` is cographic. + If there is some entry not in `\{0, 1\}`, return ``False``. + + This method is based on Seymour's decomposition. + The decomposition will stop once noncographicness is detected. + For direct cographicness check, + + .. SEEALSO:: + + - :meth:`sage.matrix.matrix_cmr_sparse.Matrix_cmr_chr_sparse._is_binary_linear_matroid_cographic` + - :meth:`UnknownNode._is_binary_linear_matroid_cographic` + - :meth:`_binary_linear_matroid_complete_decomposition` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: from sage.matrix.seymour_decomposition import DecompositionNode + sage: M2 = Matrix_cmr_chr_sparse.one_sum([[1, 0], [1, 1], [0, 1]], + ....: [[1, 1, 0], [0, 1, 1]]) + sage: result, certificate = M2.is_totally_unimodular(certificate=True, + ....: row_keys=range(5), + ....: column_keys='abcde') + sage: node = DecompositionNode.one_sum(*certificate.child_nodes()) + sage: node._cographicness() + Traceback (most recent call last): + ... + ValueError: It is not determined whether the decomposition node is cographic/conetwork + sage: result, decomposition = node._is_binary_linear_matroid_cographic(decomposition=True) + sage: result + True + sage: unicode_art(decomposition) + ╭──────────OneSumNode (5×5) with 2 children + │ │ + PlanarNode (3×2) PlanarNode (2×3) + sage: decomposition._cographicness() + True + sage: node._is_binary_linear_matroid_cographic(certificate=True) + Traceback (most recent call last): + ... + NotImplementedError + + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(GF(2), 9, 9, sparse=True), + ....: [[1, 1, 0, 0, 0, 0, 0, 0, 0], + ....: [1, 1, 1, 0, 0, 0, 0, 0, 0], + ....: [1, 0, 0, 1, 0, 0, 0, 0, 0], + ....: [0, 1, 1, 1, 0, 0, 0, 0, 0], + ....: [0, 0, 1, 1, 0, 0, 0, 0, 0], + ....: [0, 0, 0, 0, 1, 1, 1, 0, 0], + ....: [0, 0, 0, 0, 1, 1, 0, 1, 0], + ....: [0, 0, 0, 0, 0, 1, 0, 1, 1], + ....: [0, 0, 0, 0, 0, 0, 1, 1, 1]]) + sage: result, certificate = M._is_binary_linear_matroid_regular(certificate=True, + ....: row_keys=[f'r{i}' for i in range(9)], + ....: column_keys=[f'c{i}' for i in range(9)]) + sage: node = DecompositionNode.one_sum(*certificate.child_nodes()) + sage: result, decomposition = node._is_binary_linear_matroid_cographic(decomposition=True) + sage: result + False + sage: unicode_art(decomposition) + ╭───────────OneSumNode (9×9) with 2 children + │ │ + GraphicNode (5×4) UnknownNode (4×5) + """ + certificate = kwds.get('certificate', False) + try: + result = self._cographicness() + if not decomposition and not certificate: + return result + result = [result] + if decomposition: + result.append(self) + if certificate: + raise NotImplementedError + return result + except ValueError: + # compute it... wait for CMR functions + full_dec = self._binary_linear_matroid_complete_decomposition( + stop_when_noncographic=True, + check_graphic_minors_planar=True) + return full_dec._is_binary_linear_matroid_cographic( + decomposition=decomposition, + certificate=certificate) + + def _is_binary_linear_matroid_regular(self, *, decomposition=False, **kwds): + r""" + Return whether the linear matroid of ``self`` over `\GF{2}` is regular. + If there is some entry not in `\{0, 1\}`, return ``False``. + + .. SEEALSO:: + + - :meth:`sage.matrix.matrix_cmr_sparse.Matrix_cmr_chr_sparse._is_binary_linear_matroid_regular` + - :meth:`_binary_linear_matroid_complete_decomposition` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(GF(2), 9, 9, sparse=True), + ....: [[1, 1, 0, 0, 0, 0, 0, 0, 0], + ....: [1, 1, 1, 0, 0, 0, 0, 0, 0], + ....: [1, 0, 0, 1, 0, 0, 0, 0, 0], + ....: [0, 1, 1, 1, 0, 0, 0, 0, 0], + ....: [0, 0, 1, 1, 0, 0, 0, 0, 0], + ....: [0, 0, 0, 0, 1, 1, 1, 0, 0], + ....: [0, 0, 0, 0, 1, 1, 0, 1, 0], + ....: [0, 0, 0, 0, 0, 1, 0, 1, 1], + ....: [0, 0, 0, 0, 0, 0, 1, 1, 1]]) + sage: from sage.matrix.seymour_decomposition import UnknownNode + sage: node = UnknownNode(M) + sage: node._regularity() + Traceback (most recent call last): + ... + ValueError: It is not determined whether the decomposition node is regular/TU + sage: node._is_binary_linear_matroid_regular() + True + sage: C0 = node._binary_linear_matroid_complete_decomposition() + sage: C0 + OneSumNode (9×9) with 2 children + sage: result, decomposition = C0._is_binary_linear_matroid_graphic(decomposition=True) + sage: result + False + sage: unicode_art(decomposition) + ╭───────────OneSumNode (9×9) with 2 children + │ │ + GraphicNode (5×4) CographicNode (4×5) + sage: decomposition.child_nodes()[1]._graphicness() + False + sage: result, decomposition = C0._is_binary_linear_matroid_cographic(decomposition=True) + sage: result + False + sage: unicode_art(decomposition) + ╭───────────OneSumNode (9×9) with 2 children + │ │ + GraphicNode (5×4) UnknownNode (4×5) + sage: decomposition.child_nodes()[1]._graphicness() + Traceback (most recent call last): + ... + ValueError: It is not determined whether the decomposition node is graphic/network + sage: result, decomposition = C0._is_binary_linear_matroid_regular(decomposition=True) + sage: result + True + sage: unicode_art(decomposition) + ╭───────────OneSumNode (9×9) with 2 children + │ │ + GraphicNode (5×4) CographicNode (4×5) + """ + certificate = kwds.get('certificate', False) + try: + result = self._regularity() + if not decomposition and not certificate: + return result + result = [result] + if decomposition: + result.append(self) + if certificate: + raise NotImplementedError + return result + except ValueError: + # compute it... wait for CMR functions + full_dec = self._binary_linear_matroid_complete_decomposition( + stop_when_irregular=True, + check_graphic_minors_planar=True) + return full_dec._is_binary_linear_matroid_regular( + decomposition=decomposition, + certificate=certificate) + + def _binary_linear_matroid_complete_decomposition(self, *, + time_limit=60.0, + use_direct_graphicness_test=True, + prefer_graphicness=True, + series_parallel_ok=True, + check_graphic_minors_planar=False, + stop_when_irregular=False, + stop_when_nongraphic=False, + stop_when_noncographic=False, + stop_when_nongraphic_and_noncographic=False, + three_sum_pivot_children=False, + three_sum_strategy=None, + construct_leaf_graphs=False, + construct_all_graphs=False): + r""" + Complete the Seymour's decomposition of ``self`` over `\GF{2}`. + + INPUT: + + - ``stop_when_irregular`` -- boolean; + whether to stop decomposing once irregularity is determined. + + - ``stop_when_nongraphic`` -- boolean; + whether to stop decomposing once non-graphicness is determined. + + - ``stop_when_noncographic`` -- boolean; + whether to stop decomposing once non-cographicness is determined. + + - ``stop_when_nongraphic_and_noncographic`` -- boolean; + whether to stop decomposing once non-graphicness and non-cographicness + is determined. + + For a description of other parameters, see + :meth:`sage.matrix.matrix_cmr_sparse._set_cmr_seymour_parameters` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(GF(2), 9, 9, sparse=True), + ....: [[1, 1, 0, 0, 0, 0, 0, 0, 0], + ....: [1, 1, 1, 0, 0, 0, 0, 0, 0], + ....: [1, 0, 0, 1, 0, 0, 0, 0, 0], + ....: [0, 1, 1, 1, 0, 0, 0, 0, 0], + ....: [0, 0, 1, 1, 0, 0, 0, 0, 0], + ....: [0, 0, 0, 0, 1, 1, 1, 0, 0], + ....: [0, 0, 0, 0, 1, 1, 0, 1, 0], + ....: [0, 0, 0, 0, 0, 1, 0, 1, 1], + ....: [0, 0, 0, 0, 0, 0, 1, 1, 1]]) + sage: from sage.matrix.seymour_decomposition import UnknownNode + sage: node = UnknownNode(M); node + UnknownNode (9×9) + sage: C0 = node._binary_linear_matroid_complete_decomposition() + sage: C0 + OneSumNode (9×9) with 2 children + sage: unicode_art(C0) + ╭───────────OneSumNode (9×9) with 2 children + │ │ + GraphicNode (5×4) CographicNode (4×5) + sage: C1, C2 = C0.child_nodes() + sage: C11 = C1._binary_linear_matroid_complete_decomposition(); C11 + GraphicNode (5×4) + sage: unicode_art(C11) + GraphicNode (5×4) + sage: C1.matrix() + [1 1 0 0] + [1 1 1 0] + [0 1 1 1] + [1 0 0 1] + [0 0 1 1] + sage: C11.matrix() + [1 1 0 0] + [1 1 1 0] + [0 1 1 1] + [1 0 0 1] + [0 0 1 1] + sage: C22 = C2._binary_linear_matroid_complete_decomposition(); C22 + CographicNode (4×5) + sage: unicode_art(C22) + CographicNode (4×5) + sage: C2.matrix() + [1 1 1 0 0] + [0 0 1 1 1] + [0 1 0 1 1] + [1 1 0 1 0] + sage: C22.matrix() + [1 1 1 0 0] + [0 0 1 1 1] + [0 1 0 1 1] + [1 1 0 1 0] + + This is test ``TreeFlagsStopNoncographic`` in CMR's ``test_regular.cpp``. + The default settings will not do the planarity check:: + + sage: unicode_art(node) + UnknownNode (9×9) + sage: certificate1 = node._binary_linear_matroid_complete_decomposition( + ....: stop_when_noncographic=True, + ....: check_graphic_minors_planar=True) + sage: unicode_art(certificate1) + ╭───────────OneSumNode (9×9) with 2 children + │ │ + GraphicNode (5×4) UnknownNode (4×5) + sage: certificate2 = node._binary_linear_matroid_complete_decomposition( + ....: stop_when_noncographic=True) + sage: unicode_art(certificate2) + ╭───────────OneSumNode (9×9) with 2 children + │ │ + GraphicNode (5×4) CographicNode (4×5) + + sage: certificate1 = node._binary_linear_matroid_complete_decomposition( + ....: stop_when_nongraphic=True, + ....: check_graphic_minors_planar=True) + sage: unicode_art(certificate1) + ╭───────────OneSumNode (9×9) with 2 children + │ │ + GraphicNode (5×4) UnknownNode (4×5) + sage: C1, C2 = certificate1.child_nodes() + sage: C1._cographicness() + False + sage: C2._graphicness() + False + sage: certificate2 = node._binary_linear_matroid_complete_decomposition( + ....: stop_when_nongraphic=True) + sage: unicode_art(certificate2) + ╭───────────OneSumNode (9×9) with 2 children + │ │ + GraphicNode (5×4) UnknownNode (4×5) + sage: C1, C2 = certificate2.child_nodes() + sage: C1._cographicness() + Traceback (most recent call last): + ... + ValueError: It is not determined whether the decomposition node is cographic/conetwork + sage: C2._graphicness() + False + """ + cdef CMR_REGULAR_PARAMS params + cdef CMR_REGULAR_STATS stats + cdef CMR_SEYMOUR_NODE *clone = NULL + + cdef CMR_SEYMOUR_NODE **pclone = &clone + + if self._dec == NULL: + base_ring = self.base_ring() + if base_ring is None: + from sage.rings.finite_rings.finite_field_constructor import GF + self._base_ring = GF(2) + elif base_ring.characteristic() != 2: + raise ValueError(f'only defined over binary, got {base_ring}') + self._set_root_dec() + + cdef dict kwds = dict(use_direct_graphicness_test=use_direct_graphicness_test, + prefer_graphicness=prefer_graphicness, + series_parallel_ok=series_parallel_ok, + check_graphic_minors_planar=check_graphic_minors_planar, + stop_when_irregular=stop_when_irregular, + stop_when_nongraphic=stop_when_nongraphic, + stop_when_noncographic=stop_when_noncographic, + stop_when_nongraphic_and_noncographic=stop_when_nongraphic_and_noncographic, + three_sum_pivot_children=three_sum_pivot_children, + three_sum_strategy=three_sum_strategy, + construct_leaf_graphs=construct_leaf_graphs, + construct_all_graphs=construct_all_graphs) + _set_cmr_seymour_parameters(¶ms.seymour, kwds) + + sig_on() + try: + CMR_CALL(CMRseymourCloneUnknown(cmr, self._dec, pclone)) + CMR_CALL(CMRregularCompleteDecomposition(cmr, clone, ¶ms, &stats, time_limit)) + finally: + sig_off() + node = create_DecompositionNode(clone, matrix=self.matrix(), + row_keys=self.row_keys(), + column_keys=self.column_keys(), + base_ring=self.base_ring()) + return node + + def is_network_matrix(self, *, decomposition=False, **kwds): + r""" + Return whether the matrix ``self`` over `\GF{3}` or `\QQ` is a network matrix. + If there is some entry not in `\{-1, 0, 1\}`, return ``False``. + + This method is based on Seymour's decomposition. + The decomposition will stop once being nonnetwork is detected. + For direct network matrix check, + + .. SEEALSO:: + + - :meth:`sage.matrix.matrix_cmr_sparse.Matrix_cmr_chr_sparse.is_network_matrix` + - :meth:`UnknownNode.is_network_matrix` + - :meth:`complete_decomposition` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: from sage.matrix.seymour_decomposition import DecompositionNode + sage: A = matrix(ZZ, [[-1, 0, 0, 0, 1,-1, 0], + ....: [ 1, 0, 0, 1,-1, 1, 0], + ....: [ 0,-1, 0,-1, 1,-1, 0], + ....: [ 0, 1, 0, 0, 0, 0, 1], + ....: [ 0, 0, 1,-1, 1, 0, 1], + ....: [ 0, 0,-1, 1,-1, 0, 0]]) + sage: M1 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 6, 7, sparse=True), A) + sage: M2 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 7, 6, sparse=True), A.transpose()) + sage: M = Matrix_cmr_chr_sparse.one_sum(M1, M2) + sage: result, certificate = M.is_totally_unimodular(certificate=True, + ....: row_keys=[f'r{i}' for i in range(13)], + ....: column_keys=[f'c{i}' for i in range(13)]) + sage: node = DecompositionNode.one_sum(*certificate.child_nodes()) + + sage: result, decomposition = node.is_network_matrix(decomposition=True) + sage: unicode_art(decomposition) + ╭─────────OneSumNode (13×13) with 2 children + │ │ + PlanarNode (6×7) PlanarNode (7×6) + sage: node._graphicness() + Traceback (most recent call last): + ... + ValueError: It is not determined whether the decomposition node is graphic/network + + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 9, 9, sparse=True), + ....: [[1, 1, 0, 0, 0, 0, 0, 0, 0], + ....: [-1,-1, 1, 0, 0, 0, 0, 0, 0], + ....: [1, 0, 0, 1, 0, 0, 0, 0, 0], + ....: [0, 1,-1,-1, 0, 0, 0, 0, 0], + ....: [0, 0, 1, 1, 0, 0, 0, 0, 0], + ....: [0, 0, 0, 0, 1,-1, 1, 0, 0], + ....: [0, 0, 0, 0, 1,-1, 0, 1, 0], + ....: [0, 0, 0, 0, 0, 1, 0,-1, 1], + ....: [0, 0, 0, 0, 0, 0, 1,-1, 1]]) + sage: result, certificate = M.is_totally_unimodular(certificate=True, + ....: row_keys=[f'r{i}' for i in range(9)], + ....: column_keys=[f'c{i}' for i in range(9)]) + sage: node = DecompositionNode.one_sum(*certificate.child_nodes()) + sage: result, decomposition = node.is_network_matrix(decomposition=True) + sage: result + False + sage: unicode_art(decomposition) + ╭───────────OneSumNode (9×9) with 2 children + │ │ + GraphicNode (5×4) UnknownNode (4×5) + sage: decomposition.child_nodes()[1]._graphicness() + False + """ + certificate = kwds.get('certificate', False) + try: + result = self._graphicness() + if not decomposition and not certificate: + return result + result = [result] + if decomposition: + result.append(self) + if certificate: + raise NotImplementedError + return result + except ValueError: + # compute it... wait for CMR functions + full_dec = self.complete_decomposition( + stop_when_nonnetwork=True, + check_graphic_minors_planar=True) + return full_dec.is_network_matrix( + decomposition=decomposition, + certificate=certificate) + + def is_conetwork_matrix(self, *, decomposition=False, **kwds): + r""" + Return whether the matrix ``self`` over `\GF{3}` or `\QQ` is a conetwork matrix. + If there is some entry not in `\{-1, 0, 1\}`, return ``False``. + + This method is based on Seymour's decomposition. + The decomposition will stop once being nonconetwork is detected. + For direct conetwork matrix check, + + .. SEEALSO:: + + - :meth:`sage.matrix.matrix_cmr_sparse.Matrix_cmr_chr_sparse.is_conetwork_matrix` + - :meth:`UnknownNode.is_conetwork_matrix` + - :meth:`complete_decomposition` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: from sage.matrix.seymour_decomposition import DecompositionNode + sage: A = matrix(ZZ, [[-1, 0, 0, 0, 1,-1, 0], + ....: [ 1, 0, 0, 1,-1, 1, 0], + ....: [ 0,-1, 0,-1, 1,-1, 0], + ....: [ 0, 1, 0, 0, 0, 0, 1], + ....: [ 0, 0, 1,-1, 1, 0, 1], + ....: [ 0, 0,-1, 1,-1, 0, 0]]) + sage: M1 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 6, 7, sparse=True), A) + sage: M2 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 7, 6, sparse=True), A.transpose()) + sage: M = Matrix_cmr_chr_sparse.one_sum(M1, M2) + sage: result, certificate = M.is_totally_unimodular(certificate=True, + ....: row_keys=[f'r{i}' for i in range(13)], + ....: column_keys=[f'c{i}' for i in range(13)]) + sage: node = DecompositionNode.one_sum(*certificate.child_nodes()) + + sage: result, decomposition = node.is_conetwork_matrix(decomposition=True) + sage: unicode_art(decomposition) + ╭─────────OneSumNode (13×13) with 2 children + │ │ + PlanarNode (6×7) PlanarNode (7×6) + sage: node._cographicness() + Traceback (most recent call last): + ... + ValueError: It is not determined whether the decomposition node is cographic/conetwork + + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 9, 9, sparse=True), + ....: [[1, 1, 0, 0, 0, 0, 0, 0, 0], + ....: [-1,-1, 1, 0, 0, 0, 0, 0, 0], + ....: [1, 0, 0, 1, 0, 0, 0, 0, 0], + ....: [0, 1,-1,-1, 0, 0, 0, 0, 0], + ....: [0, 0, 1, 1, 0, 0, 0, 0, 0], + ....: [0, 0, 0, 0, 1,-1, 1, 0, 0], + ....: [0, 0, 0, 0, 1,-1, 0, 1, 0], + ....: [0, 0, 0, 0, 0, 1, 0,-1, 1], + ....: [0, 0, 0, 0, 0, 0, 1,-1, 1]]) + sage: result, certificate = M.is_totally_unimodular(certificate=True, + ....: row_keys=[f'r{i}' for i in range(9)], + ....: column_keys=[f'c{i}' for i in range(9)]) + sage: node = DecompositionNode.one_sum(*certificate.child_nodes()) + sage: result, decomposition = node.is_conetwork_matrix(decomposition=True) + sage: result + False + sage: unicode_art(decomposition) + ╭───────────OneSumNode (9×9) with 2 children + │ │ + GraphicNode (5×4) UnknownNode (4×5) + sage: decomposition.child_nodes()[1]._graphicness() + Traceback (most recent call last): + ... + ValueError: It is not determined whether the decomposition node is graphic/network + """ + certificate = kwds.get('certificate', False) + try: + result = self._cographicness() + if not decomposition and not certificate: + return result + result = [result] + if decomposition: + result.append(self) + if certificate: + raise NotImplementedError + return result + except ValueError: + # compute it... wait for CMR functions + full_dec = self.complete_decomposition( + stop_when_nonconetwork=True, + check_graphic_minors_planar=True) + return full_dec.is_conetwork_matrix( + decomposition=decomposition, + certificate=certificate) + + def is_totally_unimodular(self, *, decomposition=False, **kwds): + r""" + Return whether ``self`` is a totally unimodular matrix. + + A matrix is totally unimodular if every subdeterminant is `0`, `1`, or `-1`. + + .. SEEALSO:: + + - :meth:`sage.matrix.matrix_cmr_sparse.Matrix_cmr_chr_sparse.is_totally_unimodular` + - :meth:`complete_decomposition` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: from sage.matrix.seymour_decomposition import DecompositionNode + sage: A = matrix(ZZ, [[-1, 0, 0, 0, 1,-1, 0], + ....: [ 1, 0, 0, 1,-1, 1, 0], + ....: [ 0,-1, 0,-1, 1,-1, 0], + ....: [ 0, 1, 0, 0, 0, 0, 1], + ....: [ 0, 0, 1,-1, 1, 0, 1], + ....: [ 0, 0,-1, 1,-1, 0, 0]]) + sage: M1 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 6, 7, sparse=True), A) + sage: M2 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 7, 6, sparse=True), A.transpose()) + sage: M = Matrix_cmr_chr_sparse.one_sum(M1, M2) + sage: result, certificate = M.is_totally_unimodular(certificate=True, + ....: row_keys=[f'r{i}' for i in range(13)], + ....: column_keys=[f'c{i}' for i in range(13)]) + sage: node = DecompositionNode.one_sum(*certificate.child_nodes()) + + sage: result, decomposition = node.is_totally_unimodular(decomposition=True) + sage: unicode_art(decomposition) + ╭─────────OneSumNode (13×13) with 2 children + │ │ + PlanarNode (6×7) PlanarNode (7×6) + sage: node._regularity() + Traceback (most recent call last): + ... + ValueError: It is not determined whether the decomposition node is regular/TU + + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 9, 9, sparse=True), + ....: [[1, 1, 0, 0, 0, 0, 0, 0, 0], + ....: [-1,-1, 1, 0, 0, 0, 0, 0, 0], + ....: [1, 0, 0, 1, 0, 0, 0, 0, 0], + ....: [0, 1,-1,-1, 0, 0, 0, 0, 0], + ....: [0, 0, 1, 1, 0, 0, 0, 0, 0], + ....: [0, 0, 0, 0, 1,-1, 1, 0, 0], + ....: [0, 0, 0, 0, 1,-1, 0, 1, 0], + ....: [0, 0, 0, 0, 0, 1, 0,-1, 1], + ....: [0, 0, 0, 0, 0, 0, 1,-1, 1]]) + sage: result, certificate = M.is_totally_unimodular(certificate=True, + ....: row_keys=[f'r{i}' for i in range(9)], + ....: column_keys=[f'c{i}' for i in range(9)]) + sage: node = DecompositionNode.one_sum(*certificate.child_nodes()) + sage: result, decomposition = node.is_totally_unimodular(decomposition=True) + sage: result + True + sage: unicode_art(decomposition) + ╭───────────OneSumNode (9×9) with 2 children + │ │ + GraphicNode (5×4) CographicNode (4×5) + """ + certificate = kwds.get('certificate', False) + try: + result = self._regularity() + if not decomposition and not certificate: + return result + result = [result] + if decomposition: + result.append(self) + if certificate: + raise NotImplementedError + return result + except ValueError: + # compute it... wait for CMR functions + full_dec = self.complete_decomposition( + stop_when_nonTU=True, + check_graphic_minors_planar=True) + return full_dec.is_totally_unimodular( + decomposition=decomposition, + certificate=certificate) + + def nminors(self): + r""" + Return the number of minors of the node. + """ + if self._minors is not None: + return len(self._minors) + if self._dec == NULL: + return 0 + return CMRseymourNumMinors(self._dec) + + def _create_minor(self, index): + r""" + A minor of a represented matroid is another one obtained + by deleting or contracting elements. In the reduced matrix representation, + an element associated with a row (resp. column) can be contracted (resp. deleted) + by removing the corresponding matrix row (resp. column). + In order to delete a row element or contract a column element, + one must first pivot such that the element type (row/column) is changed. + + A minor of a matroid represented by matrix `M` is represented + by means of a ``CMR_MINOR`` object. + It consists of a (potentially empty) array of pivots and a ``CMR_SUBMAT`` object + indicating a submatrix `M'` of the matrix obtained from `M` after applying the pivots. + Moreover, the type field indicates a certain structure of `M'`: + + - A determinant ``CMR_MINOR_TYPE_DETERMINANT`` + indicates that `M'` is a submatrix of `M` with `|\det(M')| \geq 2`. + In particular, no pivots are applied. + - A Fano ``CMR_MINOR_TYPE_FANO`` + indicates that `M'` represents the Fano matroid `F_7`. + - A Fano-dual ``CMR_MINOR_TYPE_FANO_DUAL`` + indicates that `M'` represents the dual `F_7^\star` of the Fano matroid. + - A K5 ``CMR_MINOR_TYPE_K5`` + indicates that `M'` represents the graphic matroid `M(K_5)` of the complete graph `K_5`. + - A K5-dual ``CMR_MINOR_TYPE_K5_DUAL`` + indicates that `M'` represents the dual matroid `M(K_5)^\star` of the complete graph `K_5`. + - A K33 ``CMR_MINOR_TYPE_K33`` + indicates that `M'` represents the graphic matroid `M(K_{3,3})` + of the complete bipartite graph `K_{3,3}`. + - A K33-dual ``CMR_MINOR_TYPE_K33_DUAL`` + indicates that `M'` represents the dual matroid `M(K_{3,3})^\star` + of the complete bipartite graph `K_{3,3}`. + """ + cdef CMR_MINOR * minor = CMRseymourMinor(self._dec, index) + cdef CMR_MINOR_TYPE typ = CMRminorType(minor) + cdef size_t npivots = CMRminorNumPivots(minor) + cdef size_t *pivot_rows = CMRminorPivotRows(minor) + cdef size_t *pivot_columns = CMRminorPivotColumns(minor) + cdef CMR_SUBMAT *submat = CMRminorSubmatrix(minor) + # TODO: consider the pivots information and the corresponding keys? + pivots_tuple = tuple((pivot_rows[i], pivot_columns[i]) for i in range(npivots)) + submat_tuple = (tuple(submat.rows[i] for i in range(submat.numRows)), + tuple(submat.columns[i] for i in range(submat.numColumns))) + import sage.matroids.matroids_catalog as matroids + from sage.graphs.graph_generators import graphs + from sage.matroids.matroid import Matroid + + if typ == CMR_MINOR_TYPE_FANO: + return matroids.catalog.Fano() + if typ == CMR_MINOR_TYPE_FANO_DUAL: + return matroids.catalog.Fano().dual() + if typ == CMR_MINOR_TYPE_K5: + return matroids.CompleteGraphic(5) + if typ == CMR_MINOR_TYPE_K5_DUAL: + return matroids.CompleteGraphic(5).dual() + if typ == CMR_MINOR_TYPE_K33: + E = 'abcdefghi' + G = graphs.CompleteBipartiteGraph(3, 3) + return Matroid(groundset=E, graph=G, regular=True) + if typ == CMR_MINOR_TYPE_K33_DUAL: + return matroids.catalog.K33dual() + if typ == CMR_MINOR_TYPE_DETERMINANT: + return '|det| = 2 submatrix', submat_tuple + if typ == CMR_MINOR_TYPE_ENTRY: + return 'bad entry' + if typ == CMR_MINOR_TYPE_CUSTOM: + return 'custom' + + def minors(self): + r""" + Return a tuple of minors of ``self``. + + Currently, it only handles determinant 2 submatrix. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 9, 9, sparse=True), + ....: [[1, 1, 0, 0, 0, 0, 0, 0, 0], + ....: [1, 1, 1, 0, 0, 0, 0, 0, 0], + ....: [1, 0, 0, 1, 0, 0, 0, 0, 0], + ....: [0, 1, 1, 1, 0, 0, 0, 0, 0], + ....: [0, 0, 1, 1, 0, 0, 0, 0, 0], + ....: [0, 0, 0, 0, 1, 1, 1, 0, 0], + ....: [0, 0, 0, 0, 1, 1, 0, 1, 0], + ....: [0, 0, 0, 0, 0, 1, 0, 1, 1], + ....: [0, 0, 0, 0, 0, 0, 1, 1, 1]]) + sage: from sage.matrix.seymour_decomposition import UnknownNode + sage: C0 = UnknownNode(M).complete_decomposition() + sage: C1, C2 = C0.child_nodes() + sage: C1.minors() + (('|det| = 2 submatrix', ((4, 3, 1), (2, 3, 0))),) + sage: C2.minors() + (('|det| = 2 submatrix', ((2, 1, 0), (4, 2, 1))),) + """ + if self._minors is not None: + return self._minors + minors_tuple = tuple(self._create_minor(index) + for index in range(self.nminors())) + self._minors = minors_tuple + return self._minors + + def complete_decomposition(self, *, time_limit=60.0, + use_direct_graphicness_test=True, + prefer_graphicness=True, + series_parallel_ok=True, + check_graphic_minors_planar=False, + stop_when_nonTU=False, + stop_when_nonnetwork=False, + stop_when_nonconetwork=False, + stop_when_nonnetwork_and_nonconetwork=False, + three_sum_pivot_children=False, + three_sum_strategy=None, + construct_leaf_graphs=False, + construct_all_graphs=False): + r""" + Complete the Seymour's decomposition of ``self`` over `\GF{3}` or `\ZZ`. + + INPUT: + + - ``stop_when_nonTU`` -- boolean; + whether to stop decomposing once being non-TU is determined. + + - ``stop_when_nonnetwork`` -- boolean; + whether to stop decomposing once being non-network is determined. + + - ``stop_when_nonconetwork`` -- boolean; + whether to stop decomposing once being non-conetwork is determined. + + - ``stop_when_nonnetwork_and_nonconetwork`` -- boolean; + whether to stop decomposing once not being network + and not being conetwork is determined. + + For a description of other parameters, see + :meth:`sage.matrix.matrix_cmr_sparse._set_cmr_seymour_parameters`. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 9, 9, sparse=True), + ....: [[1, 1, 0, 0, 0, 0, 0, 0, 0], + ....: [1, 1, 1, 0, 0, 0, 0, 0, 0], + ....: [1, 0, 0, 1, 0, 0, 0, 0, 0], + ....: [0, 1, 1, 1, 0, 0, 0, 0, 0], + ....: [0, 0, 1, 1, 0, 0, 0, 0, 0], + ....: [0, 0, 0, 0, 1, 1, 1, 0, 0], + ....: [0, 0, 0, 0, 1, 1, 0, 1, 0], + ....: [0, 0, 0, 0, 0, 1, 0, 1, 1], + ....: [0, 0, 0, 0, 0, 0, 1, 1, 1]]) + sage: from sage.matrix.seymour_decomposition import UnknownNode + sage: node = UnknownNode(M); node + UnknownNode (9×9) + sage: C0 = node.complete_decomposition() + sage: unicode_art(C0) + ╭OneSumNode (9×9) with 2 children─╮ + │ │ + ThreeConnectedIrregularNode (5×4) ThreeConnectedIrregularNode (4×5) + sage: C1, C2 = C0.child_nodes() + sage: C11 = C1.complete_decomposition(); C11 + ThreeConnectedIrregularNode (5×4) + sage: unicode_art(C11) + ThreeConnectedIrregularNode (5×4) + sage: C1.matrix() + [1 1 0 0] + [1 1 1 0] + [0 1 1 1] + [1 0 0 1] + [0 0 1 1] + sage: C11.matrix() + [1 1 0 0] + [1 1 1 0] + [0 1 1 1] + [1 0 0 1] + [0 0 1 1] + sage: C22 = C2.complete_decomposition(); C22 + ThreeConnectedIrregularNode (4×5) + sage: unicode_art(C22) + ThreeConnectedIrregularNode (4×5) + sage: C2.matrix() + [1 1 1 0 0] + [0 0 1 1 1] + [0 1 0 1 1] + [1 1 0 1 0] + sage: C22.matrix() + [1 1 1 0 0] + [0 0 1 1 1] + [0 1 0 1 1] + [1 1 0 1 0] + """ + cdef CMR_TU_PARAMS params + cdef CMR_TU_STATS stats + cdef CMR_SEYMOUR_NODE *clone = NULL + + cdef CMR_SEYMOUR_NODE **pclone = &clone + + if self._dec == NULL: + if self.base_ring() is None: + self._base_ring = ZZ + self._set_root_dec() + + cdef dict kwds = dict(use_direct_graphicness_test=use_direct_graphicness_test, + prefer_graphicness=prefer_graphicness, + series_parallel_ok=series_parallel_ok, + check_graphic_minors_planar=check_graphic_minors_planar, + stop_when_irregular=stop_when_nonTU, + stop_when_nongraphic=stop_when_nonnetwork, + stop_when_noncographic=stop_when_nonconetwork, + stop_when_nongraphic_and_noncographic=stop_when_nonnetwork_and_nonconetwork, + three_sum_pivot_children=three_sum_pivot_children, + three_sum_strategy=three_sum_strategy, + construct_leaf_graphs=construct_leaf_graphs, + construct_all_graphs=construct_all_graphs) + params.algorithm = CMR_TU_ALGORITHM_DECOMPOSITION + params.ternary = True + params.camionFirst = False + _set_cmr_seymour_parameters(¶ms.seymour, kwds) + + sig_on() + try: + CMR_CALL(CMRseymourCloneUnknown(cmr, self._dec, pclone)) + CMR_CALL(CMRtuCompleteDecomposition(cmr, clone, ¶ms, &stats, time_limit)) + finally: + sig_off() + node = create_DecompositionNode(clone, matrix=self.matrix(), + row_keys=self.row_keys(), + column_keys=self.column_keys(), + base_ring=self.base_ring()) + return node + + +cdef class ThreeConnectedIrregularNode(DecompositionNode): + + pass + + +cdef class UnknownNode(DecompositionNode): + r""" + EXAMPLES:: + + sage: from sage.matrix.seymour_decomposition import UnknownNode + sage: node = UnknownNode([[1, 1], [0, 1]]); node + UnknownNode (2×2) + sage: node.matrix() + [1 1] + [0 1] + sage: node = UnknownNode(matrix(ZZ, [[1, 0, 1], [0, 1, 1]])); node + UnknownNode (2×3) + sage: node.matrix() + [1 0 1] + [0 1 1] + sage: node = UnknownNode(matrix(ZZ, [[1, 0, 1], [0, 1, 1]]), + ....: row_keys='ab', + ....: column_keys=range(3)); node + UnknownNode (2×3) + sage: node.matrix() + [1 0 1] + [0 1 1] + sage: node.morphism()._unicode_art_matrix() + 0 1 2 + a⎛1 0 1⎞ + b⎝0 1 1⎠ + + From a module morphism:: + + sage: phi = matrix(ZZ, [[1, 0, 1], [0, 1, 1]], + ....: row_keys='ab', column_keys=range(3)); phi + Generic morphism: + From: Free module generated by {0, 1, 2} over Integer Ring + To: Free module generated by {'a', 'b'} over Integer Ring + sage: node = UnknownNode(phi); node + UnknownNode (2×3) + sage: node.matrix() + [1 0 1] + [0 1 1] + """ + + def _is_binary_linear_matroid_graphic(self, *, decomposition=False, certificate=False, **kwds): + r""" + Return whether the linear matroid of ``self`` over `\GF{2}` is graphic. + If there is some entry not in `\{0, 1\}`, return ``False``. + + This is an internal method because it should really be exposed + as a method of :class:`Matroid`. + + .. SEEALSO:: + + :meth:`sage.matrix.matrix_cmr_sparse.Matrix_cmr_chr_sparse._is_binary_linear_matroid_graphic` + + EXAMPLES:: + + sage: from sage.matrix.seymour_decomposition import UnknownNode + sage: node = UnknownNode([[1, 0], [1, 1], [0, 1]]); node + UnknownNode (3×2) + sage: node.matrix() + [1 0] + [1 1] + [0 1] + sage: node._is_binary_linear_matroid_graphic() + True + sage: result, certificate = node._is_binary_linear_matroid_graphic(certificate=True) + sage: graph, forest_edges, coforest_edges = certificate + sage: graph.vertices(sort=True) # the numbers have no meaning + [1, 2, 7, 12] + sage: graph.edges(sort=True, labels=False) + [(1, 2), (1, 7), (1, 12), (2, 7), (7, 12)] + sage: forest_edges # indexed by rows of M + ((1, 2), (7, 1), (12, 7)) + sage: coforest_edges # indexed by cols of M + ((2, 7), (1, 12)) + """ + matrix = self.matrix() + if not decomposition and not certificate: + return matrix._is_binary_linear_matroid_graphic(**kwds) + result, cert = matrix._is_binary_linear_matroid_graphic(certificate=True, + row_keys=self.row_keys(), + column_keys=self.column_keys(), **kwds) + result = [result] + if decomposition: + graph, forest_edges, coforest_edges = cert + node = GraphicNode(matrix, graph, forest_edges, coforest_edges) + result.append(node) + if certificate: + result.append(cert) + return result + + def _is_binary_linear_matroid_cographic(self, *, decomposition=False, certificate=False, **kwds): + r""" + Return whether the linear matroid of ``self`` over `\GF{2}` is cographic. + If there is some entry not in `\{0, 1\}`, return ``False``. + + This is an internal method because it should really be exposed + as a method of :class:`Matroid`. + + .. SEEALSO:: + + :meth:`sage.matrix.matrix_cmr_sparse.Matrix_cmr_chr_sparse._is_binary_linear_matroid_cographic` + + EXAMPLES:: + + sage: from sage.matrix.seymour_decomposition import UnknownNode + sage: node = UnknownNode([[1, 1, 0], [0, 1, 1]]); node + UnknownNode (2×3) + sage: node.matrix() + [1 1 0] + [0 1 1] + sage: node._is_binary_linear_matroid_cographic() + True + sage: result, certificate = node._is_binary_linear_matroid_cographic(certificate=True) + sage: graph, forest_edges, coforest_edges = certificate + sage: graph.vertices(sort=True) # the numbers have no meaning + [1, 2, 7, 12] + sage: graph.edges(sort=True, labels=False) + [(1, 2), (1, 7), (1, 12), (2, 7), (7, 12)] + sage: forest_edges # indexed by rows of M + ((1, 2), (7, 1)) + sage: coforest_edges # indexed by cols of M + ((2, 7), (1, 12), (1, 2)) + """ + matrix = self.matrix() + if not decomposition and not certificate: + return matrix._is_binary_linear_matroid_cographic(**kwds) + result, cert = matrix._is_binary_linear_matroid_cographic(certificate=True, + row_keys=self.row_keys(), + column_keys=self.column_keys(), **kwds) + result = [result] + if decomposition: + graph, forest_edges, coforest_edges = cert + node = CographicNode(matrix, graph, forest_edges, coforest_edges) + result.append(node) + if certificate: + result.append(cert) + return result + + def is_network_matrix(self, *, decomposition=False, certificate=False, **kwds): + r""" + Return whether the matrix ``self`` over `\GF{3}` or `\QQ` is a network matrix. + If there is some entry not in `\{-1, 0, 1\}`, return ``False``. + + .. SEEALSO:: + + :meth:`sage.matrix.matrix_cmr_sparse.Matrix_cmr_chr_sparse.is_network_matrix` + + EXAMPLES:: + + sage: from sage.matrix.seymour_decomposition import UnknownNode + sage: node = UnknownNode([[1, 0], [-1, 1], [0, -1]]); node + UnknownNode (3×2) + sage: node.matrix() + [ 1 0] + [-1 1] + [ 0 -1] + sage: node.is_network_matrix() + True + sage: result, certificate = node.is_network_matrix(certificate=True) + sage: graph, forest_edges, coforest_edges = certificate + sage: graph + Digraph on 4 vertices + sage: graph.vertices(sort=True) # the numbers have no meaning + [1, 2, 7, 12] + sage: graph.edges(sort=True, labels=False) + [(2, 1), (2, 7), (7, 1), (7, 12), (12, 1)] + sage: forest_edges # indexed by rows of M + ((2, 1), (7, 1), (7, 12)) + sage: coforest_edges # indexed by cols of M + ((2, 7), (12, 1)) + """ + matrix = self.matrix() + if not decomposition and not certificate: + return matrix.is_network_matrix(**kwds) + result, cert = matrix.is_network_matrix(certificate=True, + row_keys=self.row_keys(), + column_keys=self.column_keys(), **kwds) + result = [result] + if decomposition: + graph, forest_edges, coforest_edges = cert + node = GraphicNode(matrix, graph, forest_edges, coforest_edges) + result.append(node) + if certificate: + result.append(cert) + return result + + def is_conetwork_matrix(self, *, decomposition=False, certificate=False, **kwds): + r""" + Return whether the matrix ``self`` over `\GF{3}` or `\QQ` is a conetwork matrix. + If there is some entry not in `\{-1, 0, 1\}`, return ``False``. + + .. SEEALSO:: + + :meth:`sage.matrix.matrix_cmr_sparse.Matrix_cmr_chr_sparse.is_conetwork_matrix` + + EXAMPLES:: + + sage: from sage.matrix.seymour_decomposition import UnknownNode + sage: node = UnknownNode([[1, -1, 0], [0, 1, -1]]); node + UnknownNode (2×3) + sage: node.matrix() + [ 1 -1 0] + [ 0 1 -1] + sage: node.is_conetwork_matrix() + True + sage: result, certificate = node.is_conetwork_matrix(certificate=True) + sage: graph, forest_edges, coforest_edges = certificate + sage: graph + Digraph on 4 vertices + sage: graph.vertices(sort=True) # the numbers have no meaning + [1, 2, 7, 12] + sage: graph.edges(sort=True, labels=False) + [(2, 1), (2, 7), (7, 1), (7, 12), (12, 1)] + sage: forest_edges # indexed by rows of M + ((2, 1), (7, 1)) + sage: coforest_edges # indexed by cols of M + ((2, 7), (12, 1), (2, 1)) + """ + matrix = self.matrix() + if not decomposition and not certificate: + return matrix.is_conetwork_matrix(**kwds) + result, cert = matrix.is_conetwork_matrix(certificate=True, + row_keys=self.row_keys(), + column_keys=self.column_keys(), **kwds) + result = [result] + if decomposition: + graph, forest_edges, coforest_edges = cert + node = CographicNode(matrix, graph, forest_edges, coforest_edges) + result.append(node) + if certificate: + result.append(cert) + return result + + +cdef class SumNode(DecompositionNode): + r""" + Base class for 1-sum, 2-sum, and 3-sum nodes in Seymour's decomposition + """ + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M2 = Matrix_cmr_chr_sparse.one_sum([[1, 0], [-1, 1]], + ....: [[1, 1], [-1, 0]]) + sage: result, certificate = M2.is_totally_unimodular(certificate=True, + ....: row_keys=range(4), + ....: column_keys='abcd') + sage: print(certificate) + OneSumNode (4×4) with 2 children + """ + result = super()._repr_() + result += f' with {self.nchildren()} children' + return result + + def permuted_block_matrix(self): + r"Return (Prow, BlockMatrix, Pcolumn) so that self.matrix() == Prow * BlockMatrix * Pcolumn ????" + raise NotImplementedError + + summands = DecompositionNode.child_nodes + + def summand_matrices(self): + r""" + Return a tuple of matrices representing the child nodes. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M2 = Matrix_cmr_chr_sparse.one_sum([[1, 0], [-1, 1]], + ....: [[1, 1], [-1, 0]]) + sage: result, certificate = M2.is_totally_unimodular(certificate=True, + ....: row_keys=range(4), + ....: column_keys='abcd') + sage: certificate.summand_matrices() + ( + [ 1 0] [ 1 1] + [-1 1], [-1 0] + ) + """ + return tuple(s.matrix() for s in self.summands()) + + +cdef class OneSumNode(SumNode): + + def block_matrix_form(self): + r""" + Return the block matrix representing the one sum node. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse.one_sum([[1, 0], [-1, 1]], [[1, 1], [-1, 0]]) + sage: result, certificate = M.is_totally_unimodular(certificate=True); certificate + OneSumNode (4×4) with 2 children + sage: certificate.summand_matrices() + ( + [ 1 0] [ 1 1] + [-1 1], [-1 0] + ) + sage: certificate.block_matrix_form() + [ 1 0| 0 0] + [-1 1| 0 0] + [-----+-----] + [ 0 0| 1 1] + [ 0 0|-1 0] + + sage: M3 = Matrix_cmr_chr_sparse.one_sum([[1, 0], [-1, 1]], + ....: [[1, 1], [-1, 0]], + ....: [[1, 0], [0, 1]]); M3 + [ 1 0| 0 0| 0 0] + [-1 1| 0 0| 0 0] + [-----+-----+-----] + [ 0 0| 1 1| 0 0] + [ 0 0|-1 0| 0 0] + [-----+-----+-----] + [ 0 0| 0 0| 1 0] + [ 0 0| 0 0| 0 1] + sage: result, certificate = M3.is_totally_unimodular(certificate=True); certificate + OneSumNode (6×6) with 4 children + sage: certificate.summand_matrices() + ( + [ 1 0] [ 1 1] + [-1 1], [-1 0], [1], [1] + ) + sage: certificate.block_matrix_form() + [ 1 0| 0 0| 0| 0] + [-1 1| 0 0| 0| 0] + [-----+-----+--+--] + [ 0 0| 1 1| 0| 0] + [ 0 0|-1 0| 0| 0] + [-----+-----+--+--] + [ 0 0| 0 0| 1| 0] + [-----+-----+--+--] + [ 0 0| 0 0| 0| 1] + """ + return Matrix_cmr_chr_sparse.one_sum(*self.summand_matrices()) + + @staticmethod + def check(result_matrix, summand_matrices, summand_parent_rows_and_columns): + r""" + Check that ``result_matrix`` is a 1-sum of ``summand_matrices``. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: from sage.matrix.seymour_decomposition import OneSumNode + + sage: M2 = Matrix_cmr_chr_sparse.one_sum([[1, 0], [-1, 1]], + ....: [[1, 1], [-1, 0]]) + sage: result, certificate = M2.is_totally_unimodular(certificate=True); certificate + OneSumNode (4×4) with 2 children + sage: OneSumNode.check(M2, + ....: certificate.summand_matrices(), + ....: certificate.child_indices()) + + Symbolic identities:: + + sage: from sage.matrix.seymour_decomposition import OneSumNode + sage: R. = QQ[] + sage: A = matrix([[x, 0], [-x, 1]]) + sage: B = matrix([[x, y], [-x, 0]]) + sage: A1B = block_diagonal_matrix([A, B]) + sage: OneSumNode.check(A1B, [A, B], [([0, 1], [0, 1]), + ....: ([2, 3], [2, 3])]) + + Using program analysis:: + + sage: # optional - cutgeneratingfunctionology + sage: R. = ParametricRealField({x: 1}, {y: -1}, {z: 0}) # true example + sage: A = matrix([[x, 0], [-x, 1]]) + sage: B = matrix([[x, y], [-x, 0]]) + sage: A1B = matrix([[z, 0, 0, 0], [-x, z, 0, 0], [], []]) + sage: OneSumNode.check(A1B, [A, B], [([0, 1], [0, 1]), + ....: ([2, 3], [2, 3])]) + sage: # side-effect: R stores polynomial identities + """ + # TODO: Check that summand_parent_rows_and_columns form partitions of rows and columns + for matrix, rows_and_columns in zip(summand_matrices, summand_parent_rows_and_columns): + assert result_matrix.matrix_from_rows_and_columns(*rows_and_columns) == matrix + # TODO: Check zero blocks + + +cdef class TwoSumNode(SumNode): + + def block_matrix_form(self): + r""" + Return the block matrix representing the two sum node. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M2 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 5, sparse=True), + ....: [[1, 1, 1, 1, 1], [1, 1, 1, 0, 0], + ....: [1, 0, 1, 1, 0], [1, 0, 0, 1, 1], + ....: [1, 1, 0, 0, 1]]); M2 + [1 1 1 1 1] + [1 1 1 0 0] + [1 0 1 1 0] + [1 0 0 1 1] + [1 1 0 0 1] + sage: M3 = Matrix_cmr_chr_sparse.two_sum(M2, M2, 0, 1); M3 + [1 1 1 1|1 1 1 0 0] + [1 1 0 0|1 1 1 0 0] + [0 1 1 0|1 1 1 0 0] + [0 0 1 1|1 1 1 0 0] + [1 0 0 1|1 1 1 0 0] + [-------+---------] + [0 0 0 0|1 1 1 1 1] + [0 0 0 0|1 0 1 1 0] + [0 0 0 0|1 0 0 1 1] + [0 0 0 0|1 1 0 0 1] + sage: result, certificate = M3.is_totally_unimodular(certificate=True); certificate + TwoSumNode (9×9) with 2 children + + sage: K33 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 4, sparse=True), + ....: [[1, 1, 0, 0], [1, 1, 1, 0], + ....: [1, 0, 0,-1], [0, 1, 1, 1], + ....: [0, 0, 1, 1]]); K33 + [ 1 1 0 0] + [ 1 1 1 0] + [ 1 0 0 -1] + [ 0 1 1 1] + [ 0 0 1 1] + sage: K33_dual = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 4, 5, sparse=True), + ....: [[1, 1, 1, 0, 0], [1, 1, 0, 1, 0], + ....: [0, 1, 0, 1, 1], [0, 0,-1, 1, 1]]); K33_dual + [ 1 1 1 0 0] + [ 1 1 0 1 0] + [ 0 1 0 1 1] + [ 0 0 -1 1 1] + sage: M = Matrix_cmr_chr_sparse.two_sum(K33, K33_dual, 0, 0, + ....: nonzero_block="bottom_left"); M + [ 1 1 1 0| 0 0 0 0] + [ 1 0 0 -1| 0 0 0 0] + [ 0 1 1 1| 0 0 0 0] + [ 0 0 1 1| 0 0 0 0] + [-----------+-----------] + [ 1 1 0 0| 1 1 0 0] + [ 1 1 0 0| 1 0 1 0] + [ 0 0 0 0| 1 0 1 1] + [ 0 0 0 0| 0 -1 1 1] + sage: result1, certificate1 = M.is_totally_unimodular(certificate=True); certificate1 + TwoSumNode (8×8) with 2 children + sage: certificate1.summand_matrices() + ( + [ 1 1 1 0] + [ 1 0 0 -1] [ 1 1 1 0 0] + [ 0 1 1 1] [ 1 1 0 1 0] + [ 0 0 1 1] [ 0 1 0 1 1] + [ 1 1 0 0], [ 0 0 -1 1 1] + ) + sage: certificate1.block_matrix_form() + [ 1 1 1 0| 0 0 0 0] + [ 1 0 0 -1| 0 0 0 0] + [ 0 1 1 1| 0 0 0 0] + [ 0 0 1 1| 0 0 0 0] + [-----------+-----------] + [ 1 1 0 0| 1 1 0 0] + [ 1 1 0 0| 1 0 1 0] + [ 0 0 0 0| 1 0 1 1] + [ 0 0 0 0| 0 -1 1 1] + sage: certificate1.child_indices() + (((0, 1, 2, 3, 4), (0, 1, 2, 3)), ((4, 5, 6, 7), (0, 4, 5, 6, 7))) + sage: M_perm = M.matrix_from_rows_and_columns([4, 6, 5, 7, 0, 1, 2, 3], range(M.ncols())) + sage: M_perm + [ 1 1 0 0 1 1 0 0] + [ 0 0 0 0 1 0 1 1] + [ 1 1 0 0 1 0 1 0] + [ 0 0 0 0 0 -1 1 1] + [ 1 1 1 0 0 0 0 0] + [ 1 0 0 -1 0 0 0 0] + [ 0 1 1 1 0 0 0 0] + [ 0 0 1 1 0 0 0 0] + sage: result2, certificate2 = M_perm.is_totally_unimodular(certificate=True) + sage: certificate2.summand_matrices() + ( + [ 1 1 1 0] + [ 1 0 0 -1] [ 1 1 1 0 0] + [ 0 1 1 1] [ 0 1 0 1 1] + [ 0 0 1 1] [ 1 1 0 1 0] + [ 1 1 0 0], [ 0 0 -1 1 1] + ) + sage: certificate2.block_matrix_form() + [ 1 1 1 0| 0 0 0 0] + [ 1 0 0 -1| 0 0 0 0] + [ 0 1 1 1| 0 0 0 0] + [ 0 0 1 1| 0 0 0 0] + [-----------+-----------] + [ 1 1 0 0| 1 1 0 0] + [ 0 0 0 0| 1 0 1 1] + [ 1 1 0 0| 1 0 1 0] + [ 0 0 0 0| 0 -1 1 1] + sage: certificate2.child_indices() + (((4, 5, 6, 7, 0), (0, 1, 2, 3)), ((0, 1, 2, 3), (0, 4, 5, 6, 7))) + """ + M1, M2 = self.summand_matrices() + return Matrix_cmr_chr_sparse.two_sum(M1, M2, M1.nrows() - 1, 0, "bottom_left") + +cdef class ThreeSumNode(SumNode): + + def _children(self): + r""" + Return a tuple of the tuples of the two children + and their row and column keys. + + .. SEEALSO:: + + :meth:`sage.matrix.matrix_cmr_sparse.Matrix_cmr_chr_sparse.three_sum` + + TESTS: + + This is test ``WideWideR12`` and ``MixedMixedR12`` in CMR's ``test_tu.cpp``:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: R12 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 6, 6, sparse=True), + ....: [[1,0,1,1,0,0],[0,1,1,1,0,0],[1,0,1,0,1,1], + ....: [0,-1,0,-1,1,1],[1,0,1,0,1,0],[0,-1,0,-1,0,1]]) + sage: result, certificate = R12.is_totally_unimodular(certificate=True, + ....: three_sum_strategy="Wide_Wide", + ....: row_keys=range(6), + ....: column_keys='abcdef') + sage: certificate.child_indices() + ((0, 1, 2, 3, a, 5), (4, b, c, d, e, f)) + sage: C = certificate.child_nodes()[0] + sage: C1, C2 = C.child_nodes() + sage: C1.matrix() + [ 0 0 1 -1 -1] + [ 1 1 1 0 0] + [ 0 1 0 1 1] + [-1 0 -1 0 1] + sage: C2.matrix() + [ 1 0 1 -1 0] + [ 0 0 1 0 1] + [-1 -1 0 1 1] + [-1 -1 0 0 1] + sage: C.child_indices() + (((0, 1, a, 3), (b, c, d, e, +3+e)), ((0, 2, 3, 5), (+0+d, d, 4, e, f))) + sage: from sage.matrix.seymour_decomposition import UnknownNode + sage: node = UnknownNode(R12, + ....: row_keys=range(6), + ....: column_keys='abcdef'); node + UnknownNode (6×6) + sage: C0 = node.complete_decomposition( + ....: three_sum_strategy="Wide_Wide", + ....: ) + sage: C0 + PivotsNode (6×6) + sage: unicode_art(C0) + PivotsNode (6×6) + │ + ╭─────────────ThreeSumNode (6×6) with 2 children + │ │ + CographicNode (4×5) GraphicNode (4×5) + sage: unicode_art(node) + UnknownNode (6×6) + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: R12_large = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 9, 12, sparse=True), + ....: [[1, -1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1], + ....: [0, 0, 0, 1, -1, 0, 0, 0, 1 , 1, 1, 1], + ....: [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1], + ....: [ 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0], + ....: [ 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, -1, -1], + ....: [ 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0], + ....: [ 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, -1, -1], + ....: [ 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0], + ....: [ 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1]]) + sage: result, certificate = R12_large.is_totally_unimodular(certificate=True, + ....: three_sum_strategy="Wide_Wide", + ....: row_keys=range(9), + ....: column_keys='abcdefghijkl') + sage: C = certificate.child_nodes()[0]; C + ThreeSumNode (9×12) with 2 children + sage: C1, C2 = C.child_nodes() + sage: C1.matrix() + [ 0 0 1 1 1 1 1] + [ 1 1 0 0 0 -1 -1] + [ 1 0 -1 0 -1 -1 -1] + [ 0 1 1 0 1 0 0] + [ 0 0 0 -1 -1 0 -1] + sage: C2.matrix() + [ 1 0 0 0 0 1 -1 0 -1] + [ 0 0 1 -1 0 -1 1 0 1] + [-1 -1 1 0 1 -1 1 0 1] + [-1 -1 0 1 1 0 0 0 0] + [-1 -1 0 0 0 0 1 1 1] + [-1 -1 0 0 0 0 1 1 0] + sage: C.row_keys() + (0, i, 2, 3, 4, 5, 6, 7, 8) + sage: C.column_keys() + (a, b, c, d, e, f, g, h, 1, j, k, l) + sage: C.child_indices()[0] + ((i, 2, 7, 8, 3), (g, h, j, k, l, d, -3+d)) + sage: C.child_indices()[1] + ((i, 0, 3, 4, 5, 6), (+i+k, k, a, b, c, d, e, f, 1)) + + sage: result, certificate = R12.is_totally_unimodular(certificate=True, + ....: three_sum_strategy="Mixed_Mixed") + sage: C1, C2 = certificate.child_nodes() + sage: C1.matrix() + [ 1 0 1 1 0] + [ 0 1 1 1 0] + [ 1 0 1 0 1] + [ 0 -1 0 -1 1] + sage: C2.matrix() + [ 1 1 0 0] + [ 1 0 1 1] + [ 0 -1 1 1] + [ 1 0 1 0] + [ 0 -1 0 1] + sage: certificate.child_indices()[0] + ((r0, r1, r2, r3), (c0, c1, c2, c3, +r2+r3)) + sage: certificate.child_indices()[1] + ((+c0+c3, r2, r3, r4, r5), (c0, c3, c4, c5)) + + sage: result, certificate = R12.is_totally_unimodular(certificate=True, + ....: three_sum_strategy="Mixed_Mixed", + ....: row_keys=range(6), + ....: column_keys='abcdef') + sage: C1, C2 = certificate.child_nodes() + sage: C1.matrix() + [ 1 0 1 1 0] + [ 0 1 1 1 0] + [ 1 0 1 0 1] + [ 0 -1 0 -1 1] + sage: C2.matrix() + [ 1 1 0 0] + [ 1 0 1 1] + [ 0 -1 1 1] + [ 1 0 1 0] + [ 0 -1 0 1] + sage: certificate.child_indices()[0] + ((0, 1, 2, 3), (a, b, c, d, +2+3)) + sage: certificate.child_indices()[1] + ((+a+d, 2, 3, 4, 5), (a, d, e, f)) + """ + if self._child_nodes is not None: + return self._child_nodes + + if self.nchildren() != 2: + raise ValueError(f"ThreeSumNode has exactly two children not {self.nchildren()}!") + + self.set_default_keys() + + cdef CMR_SEYMOUR_NODE *child1_dec = CMRseymourChild(self._dec, 0) + cdef CMR_ELEMENT *parent_rows1 = CMRseymourChildRowsToParent(self._dec, 0) + cdef CMR_ELEMENT *parent_columns1 = CMRseymourChildColumnsToParent(self._dec, 0) + cdef CMR_CHRMAT *mat1 = CMRseymourGetMatrix(child1_dec) + + cdef CMR_SEYMOUR_NODE *child2_dec = CMRseymourChild(self._dec, 1) + cdef CMR_ELEMENT *parent_rows2 = CMRseymourChildRowsToParent(self._dec, 1) + cdef CMR_ELEMENT *parent_columns2 = CMRseymourChildColumnsToParent(self._dec, 1) + cdef CMR_CHRMAT *mat2 = CMRseymourGetMatrix(child2_dec) + + cdef size_t index1, index2 + + child1_nrows = CMRseymourNumRows(child1_dec) + child1_ncols = CMRseymourNumColumns(child1_dec) + + if self.is_concentrated_rank(): # Mixed_Mixed + child1_row_keys = tuple(self._CMRelement_to_key(parent_rows1[i]) + for i in range(child1_nrows)) + child1_column_keys = tuple(self._CMRelement_to_key(parent_columns1[i]) + for i in range(child1_ncols - 1)) + + row1_index = child1_nrows - 2 + CMR_CALL(CMRchrmatFindEntry(mat1, row1_index, child1_ncols-1, &index1)) + if index1 == SIZE_MAX: + eps1 = Integer(0) + else: + eps1 = Integer(mat1.entryValues[index1]) + if eps1 != 1: + raise ValueError(f"First child in the Mixed_Mixed Three Sum " + f"has 1 in the entry " + f"row {row1_index} and column {child1_ncols-1} " + f"but got {eps1}") + + row2_index = child1_nrows - 1 + CMR_CALL(CMRchrmatFindEntry(mat1, row2_index, child1_ncols-1, &index2)) + if index2 == SIZE_MAX: + eps2 = Integer(0) + else: + eps2 = Integer(mat1.entryValues[index2]) + if eps2 != 1 and eps2 != -1: + raise ValueError(f"First child in the Mixed_Mixed Three Sum " + f"has 1 or -1 in the entry " + f"row {row2_index} and column {child1_ncols-1} " + f"but got {eps2}") + + extra_key = ElementKey((eps1, child1_row_keys[row1_index], + eps2, child1_row_keys[row2_index]), + composition=True) + child1_column_keys += (extra_key,) + else: # Wide_Wide + child1_row_keys = tuple(self._CMRelement_to_key(parent_rows1[i]) + for i in range(child1_nrows)) + child1_column_keys = tuple(self._CMRelement_to_key(parent_columns1[i]) + for i in range(child1_ncols - 1)) + + row_index = child1_nrows - 1 + column_index = child1_ncols - 2 + CMR_CALL(CMRchrmatFindEntry(mat1, row_index, child1_ncols-1, &index1)) + if index1 == SIZE_MAX: + eps1 = Integer(0) + else: + eps1 = Integer(mat1.entryValues[index1]) + if eps1 != 1 and eps1 != -1: + raise ValueError(f"First child in the Wide_Wide Three Sum " + f"has 1 or -1 in the entry " + f"row {row_index} and column {child1_ncols-1} " + f"but got {eps1}") + + extra_key = ElementKey((1, child1_column_keys[column_index], + eps1, child1_row_keys[row_index]), + composition=True) + child1_column_keys += (extra_key,) + + child1 = create_DecompositionNode(child1_dec, matrix=None, + row_keys=child1_row_keys, + column_keys=child1_column_keys, + base_ring=self.base_ring()) + + child2_nrows = CMRseymourNumRows(child2_dec) + child2_ncols = CMRseymourNumColumns(child2_dec) + + if self.is_concentrated_rank(): # Mixed_Mixed + child2_row_keys = tuple(self._CMRelement_to_key(parent_rows2[i]) + for i in range(1, child2_nrows)) + child2_column_keys = tuple(self._CMRelement_to_key(parent_columns2[i]) + for i in range(child2_ncols)) + + CMR_CALL(CMRchrmatFindEntry(mat2, 0, 0, &index1)) + if index1 == SIZE_MAX: + eps1 = Integer(0) + else: + eps1 = Integer(mat1.entryValues[index1]) + if eps1 != 1 and eps1 != -1: + raise ValueError(f"Second child in the Mixed_Mixed Three Sum " + f"has 1 or -1 in the entry " + f"row {0} and column {0} " + f"but got {eps1}") + + CMR_CALL(CMRchrmatFindEntry(mat2, 0, 1, &index2)) + if index2 == SIZE_MAX: + eps2 = Integer(0) + else: + eps2 = Integer(mat1.entryValues[index2]) + if eps2 != 1: + raise ValueError(f"Second child in the Mixed_Mixed Three Sum " + f"has 1 in the entry " + f"row {0} and column {1} " + f"but got {eps2}") + + extra_key = ElementKey((eps1, child2_column_keys[0], + eps2, child2_column_keys[1]), + composition=True) + child2_row_keys = (extra_key,) + child2_row_keys + else: # Wide_Wide + child2_row_keys = tuple(self._CMRelement_to_key(parent_rows2[i]) + for i in range(child2_nrows)) + + CMR_CALL(CMRchrmatFindEntry(mat2, 0, 0, &index1)) + if index1 == SIZE_MAX: + eps1 = Integer(0) + else: + eps1 = Integer(mat1.entryValues[index1]) + + if eps1 != 1 and eps1 != -1: + raise ValueError(f"Second child in the Wide_Wide Three Sum " + f"has 1 or -1 in the entry " + f"row {0} and column {0} " + f"but got {eps1}") + + child2_column_keys = tuple(self._CMRelement_to_key(parent_columns2[i]) + for i in range(1, child2_ncols)) + extra_key = ElementKey((1, child2_column_keys[0], + eps1, child2_row_keys[0]), + composition=True) + child2_column_keys = (extra_key,) + child2_column_keys + + child2 = create_DecompositionNode(child2_dec, matrix=None, + row_keys=child2_row_keys, + column_keys=child2_column_keys, + base_ring=self.base_ring()) + + self._child_nodes = ((child1, child1_row_keys, child1_column_keys), + (child2, child2_row_keys, child2_column_keys)) + return self._child_nodes + + def is_distributed_ranks(self): + r""" + Check whether the three sum node ``self`` is formed with + ``three_sum_strategy="distributed_ranks"`` or ``"Wide_Wide"``. + + The matrix representing the first child is + `M_1=\begin{bmatrix} A & a_2 & a_2\\ a_1^T & 0 & \epsilon_2\end{bmatrix}` + and the matrix representing the second child is + `M_2=\begin{bmatrix} \epsilon_1 & 0 & b_2^T\\ b_1 & b_1 & B\end{bmatrix}`, + where `\epsilon_1`, `\epsilon_2` are `1` or `-1`. + And the matrix representing ``self`` is a permutation of + `M_1 \oplus_3 M_2 = \begin{bmatrix} A & a_2 b_2^T \\ b_1 a_1^T & B\end{bmatrix}`. + + ``distributed_ranks`` is named after the two rank 1 off-diagonal blocks. + ``Wide_Wide`` is named after the structure of the two children. + + .. SEEALSO:: + + :meth:`sage.matrix.matrix_cmr_sparse.Matrix_cmr_chr_sparse.three_sum_wide_wide` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: R12_large = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 9, 12, sparse=True), + ....: [[ 1, -1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1], + ....: [ 0, 0, 0, 1, -1, 0, 0, 0, 1, 1, 1, 1], + ....: [ 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1], + ....: [ 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0], + ....: [ 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, -1, -1], + ....: [ 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0], + ....: [ 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, -1, -1], + ....: [ 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0], + ....: [ 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1]]) + sage: result, certificate = R12_large.is_totally_unimodular(certificate=True) + sage: C = certificate.child_nodes()[0]; C + ThreeSumNode (9×12) with 2 children + sage: C.is_distributed_ranks() + True + sage: C.is_concentrated_rank() + False + """ + return CMRseymourThreeSumDistributedRanks(self._dec) + + def is_concentrated_rank(self): + r""" + Check whether the three sum node ``self`` is formed with + ``three_sum_strategy="concentrated_rank"`` or ``"Mixed_Mixed"``. + + The matrix representing the first child is + `M_1=\begin{bmatrix} A & 0 \\ a_1^T & 1\\ a_2^T & \epsilon_2\end{bmatrix}` + and the matrix representing the second child is + `M_2=\begin{bmatrix} \epsilon_1 & 1 & 0\\ b_1 & b_2 & B\end{bmatrix}`, + where `\epsilon_1`, `\epsilon_2` are `1` or `-1`. + And the matrix representing ``self`` is a permutation of + `M_1 \oplus_3 M_2 = \begin{bmatrix} A & 0 \\ C & B\end{bmatrix}`, + where `\begin{bmatrix}a_1^T \\ a_2^T\end{bmatrix}=\begin{bmatrix} C_1 & \bar{C}\end{bmatrix}`, + `\begin{bmatrix}b_1 & b_2\end{bmatrix}=\begin{bmatrix} \bar{C}\\ C_2\end{bmatrix}`, + `C_12 = C_2 \bar{C}^{-1} C_1`, + `C=\begin{bmatrix} C_1 & \bar{C} \\ C_{12} & C_2\end{bmatrix}`, i.e., + `C=\begin{bmatrix}b_1 & b_2\end{bmatrix}\bar{C}^{-1}\begin{bmatrix}a_1^T\\ a_2^T\end{bmatrix}` + + ``concentrated_rank`` is named after the rank 2 off-diagonal block. + ``Mixed_Mixed`` is named after the structure of the two children. + + .. SEEALSO:: + + :meth:`sage.matrix.matrix_cmr_sparse.Matrix_cmr_chr_sparse.three_sum_mixed_mixed` + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: R12_large = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 9, 12, sparse=True), + ....: [[ 1, -1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1], + ....: [ 0, 0, 0, 1, -1, 0, 0, 0, 1, 1, 1, 1], + ....: [ 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1], + ....: [ 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0], + ....: [ 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, -1, -1], + ....: [ 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0], + ....: [ 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, -1, -1], + ....: [ 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0], + ....: [ 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1]]) + sage: result, certificate = R12_large.is_totally_unimodular(certificate=True, + ....: three_sum_strategy="Mixed_Mixed") + sage: C = certificate; C + ThreeSumNode (9×12) with 2 children + sage: C.is_distributed_ranks() + False + sage: C.is_concentrated_rank() + True + """ + return CMRseymourThreeSumConcentratedRank(self._dec) + + def block_matrix_form(self): + r""" + Return the block matrix constructed from the three sum of children. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: R12 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 6, 6, sparse=True), + ....: [[1,0,1,1,0,0],[0,1,1,1,0,0],[1,0,1,0,1,1], + ....: [0,-1,0,-1,1,1],[1,0,1,0,1,0],[0,-1,0,-1,0,1]]) + sage: R12 + [ 1 0 1 1 0 0] + [ 0 1 1 1 0 0] + [ 1 0 1 0 1 1] + [ 0 -1 0 -1 1 1] + [ 1 0 1 0 1 0] + [ 0 -1 0 -1 0 1] + sage: result, certificate = R12.is_totally_unimodular(certificate=True) + sage: C = certificate.child_nodes()[0]; C + ThreeSumNode (6×6) with 2 children + sage: C.matrix() + [ 1 0 0 1 -1 0] + [ 0 1 1 1 0 0] + [ 1 0 0 0 0 1] + [ 0 -1 0 -1 1 1] + [-1 0 1 0 1 0] + [ 0 -1 0 -1 0 1] + sage: C.summand_matrices() + ( + [ 0 0 1 -1 -1] [ 1 0 1 -1 0] + [ 1 1 1 0 0] [ 0 0 1 0 1] + [ 0 1 0 1 1] [-1 -1 0 1 1] + [-1 0 -1 0 1], [-1 -1 0 0 1] + ) + sage: C.block_matrix_form() + [ 0 0 1 -1 1 0] + [ 1 1 1 0 0 0] + [ 0 1 0 1 -1 0] + [ 0 0 0 1 0 1] + [ 1 0 1 0 1 1] + [ 1 0 1 0 0 1] + + sage: result, certificate = R12.is_totally_unimodular(certificate=True, + ....: three_sum_strategy="Mixed_Mixed") + sage: C = certificate; C + ThreeSumNode (6×6) with 2 children + sage: C.matrix() + [ 1 0 1 1 0 0] + [ 0 1 1 1 0 0] + [ 1 0 1 0 1 1] + [ 0 -1 0 -1 1 1] + [ 1 0 1 0 1 0] + [ 0 -1 0 -1 0 1] + sage: C.summand_matrices() + ( + [ 1 1 0 0] + [ 1 0 1 1 0] [ 1 0 1 1] + [ 0 1 1 1 0] [ 0 -1 1 1] + [ 1 0 1 0 1] [ 1 0 1 0] + [ 0 -1 0 -1 1], [ 0 -1 0 1] + ) + sage: C.child_indices() + (((r0, r1, r2, r3), (c0, c1, c2, c3, +r2+r3)), + ((+c0+c3, r2, r3, r4, r5), (c0, c3, c4, c5))) + sage: C.block_matrix_form() + [ 1 0 1 1 0 0] + [ 0 1 1 1 0 0] + [ 1 0 1 0 1 1] + [ 0 -1 0 -1 1 1] + [ 1 0 1 0 1 0] + [ 0 -1 0 -1 0 1] + """ + M1, M2 = self.summand_matrices() + if self.is_distributed_ranks(): + three_sum_strategy = 'distributed_ranks' + else: + three_sum_strategy = 'concentrated_rank' + return Matrix_cmr_chr_sparse.three_sum(M1, M2, + three_sum_strategy=three_sum_strategy, + sign_verify=True) + + +cdef class BaseGraphicNode(DecompositionNode): + + def __init__(self, matrix=None, + graph=None, forest_edges=None, coforest_edges=None, + row_keys=None, column_keys=None, base_ring=None): + r""" + Base class for :class:`GraphicNode`, :class:`CographicNode`, and :class:`PlanarNode` + + If ``base_ring`` is `\GF{2}`, then it represents a graphic/cographic/planar matroid. + + Suppose that ``self.matrix()`` is a graphic matrix of a graph `G` + with respect to `T`, a spanning forest of the graph `G`. + + - ``self._graph`` is the graph `G = (V,E)`. + + - ``self._forest_edges`` is the edges of `T`. + + - ``self._coforest_edges`` is the edges of `E \setminus T`. + + If ``base_ring`` is `\GF{3}` or `\ZZ`, then it represents a network/conetwork + or a both network and conetwork matrix. + + Suppose that ``self.matrix()`` is a network matrix of a digraph `D` + with respect to `T`, a directed spanning forest of the underlying undirected graph. + + - ``self._graph`` is the digraph `D = (V,A)`. + + - ``self._forest_edges`` is the arcs of `T`. + + - ``self._coforest_edges`` is the arcs of `A \setminus T`. + """ + super().__init__(matrix=matrix, row_keys=row_keys, column_keys=column_keys, + base_ring=base_ring) + self._graph = graph + self._forest_edges = forest_edges + self._coforest_edges = coforest_edges + + def graph(self): + r""" + Return the graph representing ``self``. + + If ``self.base_ring()`` is `\GF{2}`, then return an undirected graph. + If ``self.base_ring()`` is `\GF{3}` or `\ZZ`, then return directed graph. + + EXAMPLES: + + Undirected graph:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 2, sparse=True), + ....: [[1, 0], [1, 1], [0,1]]); M + [1 0] + [1 1] + [0 1] + sage: result, certificate = M._is_binary_linear_matroid_regular(certificate=True) + sage: result, certificate + (True, GraphicNode (3×2)) + sage: G = certificate.graph(); G + Graph on 4 vertices + sage: G.vertices(sort=True) + [1, 2, 7, 12] + sage: G.edges(sort=True) + [(1, 2, None), (1, 7, None), (1, 12, None), (2, 7, None), (7, 12, None)] + + Directed graph:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 2, sparse=True), + ....: [[1, 0], [-1, 1], [0,-1]]); M + [ 1 0] + [-1 1] + [ 0 -1] + sage: result, certificate = M.is_totally_unimodular(certificate=True) + sage: result, certificate + (True, GraphicNode (3×2)) + sage: G = certificate.graph(); G + Digraph on 4 vertices + sage: G.vertices(sort=True) + [1, 2, 7, 12] + sage: G.edges(sort=True) + [(2, 1, None), (2, 7, None), (7, 1, None), (7, 12, None), (12, 1, None)] + """ + if self._graph is not None: + return self._graph + base_ring = self.base_ring() + if base_ring.characteristic() == 2: + self._graph = _sage_graph(CMRseymourGraph(self._dec)) + else: + self._graph = _sage_digraph(CMRseymourGraph(self._dec), + CMRseymourGraphArcsReversed(self._dec)) + return self._graph + + def forest_edges(self): + r""" + Return the forest edges of ``self``. + + If ``self.base_ring()`` is `\GF{2}`, then return edges. + If ``self.base_ring()`` is `\GF{3}` or `\ZZ`, then arcs. + + EXAMPLES: + + Undirected graph:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 2, sparse=True), + ....: [[1, 0], [1, 1], [0,1]]); M + [1 0] + [1 1] + [0 1] + sage: result, certificate = M._is_binary_linear_matroid_regular(certificate=True) + sage: result, certificate + (True, GraphicNode (3×2)) + sage: certificate.forest_edges() + ((1, 2), (7, 1), (12, 7)) + sage: certificate.coforest_edges() + ((2, 7), (1, 12)) + + Directed graph:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 2, sparse=True), + ....: [[1, 0], [-1, 1], [0,-1]]); M + [ 1 0] + [-1 1] + [ 0 -1] + sage: result, certificate = M.is_totally_unimodular(certificate=True) + sage: result, certificate + (True, GraphicNode (3×2)) + sage: certificate.forest_edges() + ((2, 1), (7, 1), (7, 12)) + sage: certificate.coforest_edges() + ((2, 7), (12, 1)) + + Starting with a morphism:: + + sage: from sage.matrix.seymour_decomposition import UnknownNode + sage: phi = matrix(ZZ, [[1, 0], [1, 1], [0, 1]], + ....: row_keys=['a', 'b', 'c'], column_keys=['v', 'w']) + sage: phi; phi._unicode_art_matrix() + Generic morphism: + From: Free module generated by {'v', 'w'} over Integer Ring + To: Free module generated by {'a', 'b', 'c'} over Integer Ring + v w + a⎛1 0⎞ + b⎜1 1⎟ + c⎝0 1⎠ + sage: phi_node = UnknownNode(phi) + sage: is_graphic, rephined_node = phi_node._is_binary_linear_matroid_graphic(decomposition=True) + sage: is_graphic, rephined_node + (True, GraphicNode (3×2)) + sage: rephined_node.forest_edges() + {'a': (1, 2), 'b': (7, 1), 'c': (12, 7)} + sage: phi_node # still in the dark about graphicness + UnknownNode (3×2) + """ + if self._forest_edges is not None: + return self._forest_edges + cdef CMR_GRAPH *graph = CMRseymourGraph(self._dec) + cdef size_t num_edges = CMRseymourGraphSizeForest(self._dec) + cdef CMR_GRAPH_EDGE *edges = CMRseymourGraphForest(self._dec) + cdef bool *arcs_reversed + base_ring = self.base_ring() + if base_ring.characteristic() == 2: + self._forest_edges = _sage_edges(graph, edges, num_edges, self.row_keys()) + else: + arcs_reversed = CMRseymourGraphArcsReversed(self._dec) + self._forest_edges = _sage_arcs(graph, edges, arcs_reversed, num_edges, self.row_keys()) + return self._forest_edges + + def coforest_edges(self): + r""" + Return the forest edges of ``self``. + + If ``self.base_ring()`` is `\GF{2}`, then return edges. + If ``self.base_ring()`` is `\GF{3}` or `\ZZ`, then arcs. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 6, 7, sparse=True), + ....: [[-1, 0, 0, 0, 1, -1, 0], + ....: [ 1, 0, 0, 1, -1, 1, 0], + ....: [ 0, -1, 0, -1, 1, -1, 0], + ....: [ 0, 1, 0, 0, 0, 0, 1], + ....: [ 0, 0, 1, -1, 1, 0, 1], + ....: [ 0, 0, -1, 1, -1, 0, 0]]) + sage: M.is_network_matrix() + True + sage: result, certificate = M.is_network_matrix(certificate=True) + sage: result, certificate + (True, + (Digraph on 7 vertices, + ((9, 8), (3, 8), (3, 4), (5, 4), (4, 6), (0, 6)), + ((3, 9), (5, 3), (4, 0), (0, 8), (9, 0), (4, 9), (5, 6)))) + sage: digraph, forest_arcs, coforest_arcs = certificate + sage: result, certificate = M.is_totally_unimodular(certificate=True) + sage: certificate.graph() == digraph + True + sage: certificate.forest_edges() == forest_arcs + True + sage: certificate.coforest_edges() == coforest_arcs + True + """ + if self._coforest_edges is not None: + return self._coforest_edges + cdef CMR_GRAPH *graph = CMRseymourGraph(self._dec) + cdef size_t num_edges = CMRseymourGraphSizeCoforest(self._dec) + cdef CMR_GRAPH_EDGE *edges = CMRseymourGraphCoforest(self._dec) + cdef bool *arcs_reversed + base_ring = self.base_ring() + if base_ring.characteristic() == 2: + self._coforest_edges = _sage_edges(graph, edges, num_edges, self.column_keys()) + else: + arcs_reversed = CMRseymourGraphArcsReversed(self._dec) + self._coforest_edges = _sage_arcs(graph, edges, arcs_reversed, num_edges, self.column_keys()) + return self._coforest_edges + + +cdef class GraphicNode(BaseGraphicNode): + + pass + + +cdef class CographicNode(BaseGraphicNode): + @cached_method + def graph(self): + r""" + Actually the cograph of matrix, in the case where it is not graphic. + + EXAMPLES: + + Undirected graph:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 4, 5, sparse=True), + ....: [[1, 1, 1, 0, 0], + ....: [0, 1, 1, 1, 0], + ....: [0, 0, 1, 1, 1], + ....: [1, 0, 0, 1, 1]]); M + [1 1 1 0 0] + [0 1 1 1 0] + [0 0 1 1 1] + [1 0 0 1 1] + sage: result, certificate = M._is_binary_linear_matroid_regular(certificate=True) + sage: certificate + CographicNode (4×5) + sage: certificate.base_ring() + Finite Field of size 2 + sage: G = certificate.graph(); G + Graph on 6 vertices + sage: G.vertices(sort=True) + [0, 1, 2, 5, 7, 8] + sage: G.edges(sort=True) + [(0, 2, None), (0, 5, None), (0, 7, None), (1, 2, None), (1, 5, None), + (1, 7, None), (2, 8, None), (5, 8, None), (7, 8, None)] + + Directed graph:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 4, 5, sparse=True), + ....: [[1, -1, 1, 0, 0], + ....: [0, 1, -1, 1, 0], + ....: [0, 0, 1, -1, 1], + ....: [1, 0, 0, 1, -1]]); M + [ 1 -1 1 0 0] + [ 0 1 -1 1 0] + [ 0 0 1 -1 1] + [ 1 0 0 1 -1] + sage: result, certificate = M.is_totally_unimodular(certificate=True) + sage: certificate + CographicNode (4×5) + sage: G = certificate.graph(); G + Digraph on 6 vertices + sage: G.vertices(sort=True) + [0, 1, 2, 5, 7, 8] + sage: G.edges(sort=True) + [(0, 2, None), (0, 5, None), (0, 7, None), (1, 2, None), (1, 5, None), + (1, 7, None), (2, 8, None), (5, 8, None), (7, 8, None)] + """ + if self._graph is not None: + return self._graph + base_ring = self.base_ring() + if base_ring.characteristic() == 2: + self._graph = _sage_graph(CMRseymourCograph(self._dec)) + else: + self._graph = _sage_digraph(CMRseymourCograph(self._dec), + CMRseymourCographArcsReversed(self._dec)) + return self._graph + + @cached_method + def forest_edges(self): + r""" + Return the forest edges of the cograph of ``self``. + + EXAMPLES: + + Undirected graph:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 4, 5, sparse=True), + ....: [[1, 1, 1, 0, 0], + ....: [0, 1, 1, 1, 0], + ....: [0, 0, 1, 1, 1], + ....: [1, 0, 0, 1, 1]]); M + [1 1 1 0 0] + [0 1 1 1 0] + [0 0 1 1 1] + [1 0 0 1 1] + sage: result, certificate = M._is_binary_linear_matroid_regular(certificate=True) + sage: result, certificate + (True, CographicNode (4×5)) + sage: certificate.forest_edges() + ((7, 8), (5, 0), (0, 7), (1, 7), (2, 1)) + sage: certificate.coforest_edges() + ((5, 8), (5, 1), (0, 2), (2, 8)) + + Directed graph:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 4, 5, sparse=True), + ....: [[1, -1, 1, 0, 0], + ....: [0, 1, -1, 1, 0], + ....: [0, 0, 1, -1, 1], + ....: [1, 0, 0, 1, -1]]); M + [ 1 -1 1 0 0] + [ 0 1 -1 1 0] + [ 0 0 1 -1 1] + [ 1 0 0 1 -1] + sage: result, certificate = M.is_totally_unimodular(certificate=True) + sage: result, certificate + (True, CographicNode (4×5)) + sage: certificate.forest_edges() + ((7, 8), (0, 5), (0, 7), (1, 7), (1, 2)) + sage: certificate.coforest_edges() + ((5, 8), (1, 5), (0, 2), (2, 8)) + """ + if self._forest_edges is not None: + return self._forest_edges + cdef CMR_GRAPH *graph = CMRseymourCograph(self._dec) + cdef size_t num_edges = CMRseymourCographSizeForest(self._dec) + cdef CMR_GRAPH_EDGE *edges = CMRseymourCographForest(self._dec) + cdef bool *arcs_reversed + base_ring = self.base_ring() + if base_ring.characteristic() == 2: + self._forest_edges = _sage_edges(graph, edges, num_edges, self.row_keys()) + else: + arcs_reversed = CMRseymourCographArcsReversed(self._dec) + self._forest_edges = _sage_arcs(graph, edges, arcs_reversed, num_edges, self.row_keys()) + return self._forest_edges + + @cached_method + def coforest_edges(self): + r""" + Return the forest edges of the cograph of ``self``. + """ + if self._coforest_edges is not None: + return self._coforest_edges + cdef CMR_GRAPH *graph = CMRseymourCograph(self._dec) + cdef size_t num_edges = CMRseymourCographSizeCoforest(self._dec) + cdef CMR_GRAPH_EDGE *edges = CMRseymourCographCoforest(self._dec) + cdef bool *arcs_reversed + base_ring = self.base_ring() + if base_ring.characteristic() == 2: + self._coforest_edges = _sage_edges(graph, edges, num_edges, self.column_keys()) + else: + arcs_reversed = CMRseymourCographArcsReversed(self._dec) + self._coforest_edges = _sage_arcs(graph, edges, arcs_reversed, num_edges, self.column_keys()) + return self._coforest_edges + + +cdef class PlanarNode(BaseGraphicNode): + @cached_method + def cograph(self): + r""" + Return the cograph of matrix. + + EXAMPLES: + + Undirected graph:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 2, sparse=True), + ....: [[1, 0], [1, 1], [0,1]]); M + [1 0] + [1 1] + [0 1] + sage: result, certificate = M._is_binary_linear_matroid_regular(certificate=True, + ....: check_graphic_minors_planar=True) + sage: result, certificate + (True, PlanarNode (3×2)) + sage: G = certificate.cograph(); G + Graph on 3 vertices + sage: G.vertices(sort=True) + [1, 2, 7] + sage: G.edges(sort=True) + [(1, 2, None), (1, 7, None), (2, 7, None)] + sage: certificate.cograph_forest_edges() + ((1, 2), (7, 1)) + sage: certificate.cograph_coforest_edges() + ((1, 2), (2, 7), (7, 1)) + + Directed graph:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 3, 2, sparse=True), + ....: [[1, 0], [-1, 1], [0,-1]]); M + [ 1 0] + [-1 1] + [ 0 -1] + sage: result, certificate = M.is_totally_unimodular(certificate=True, + ....: check_graphic_minors_planar=True) + sage: result, certificate + (True, PlanarNode (3×2)) + sage: G = certificate.cograph(); G + Digraph on 3 vertices + sage: G.vertices(sort=True) + [1, 2, 7] + sage: G.edges(sort=True) + [(1, 2, None), (1, 7, None), (2, 7, None), (7, 1, None)] + sage: certificate.cograph_forest_edges() + ((1, 2), (1, 7)) + sage: certificate.cograph_coforest_edges() + ((1, 2), (2, 7), (7, 1)) + """ + if self._cograph is not None: + return self._cograph + base_ring = self.base_ring() + if base_ring.characteristic() == 2: + self._cograph = _sage_graph(CMRseymourCograph(self._dec)) + else: + self._cograph = _sage_digraph(CMRseymourCograph(self._dec), + CMRseymourCographArcsReversed(self._dec)) + return self._cograph + + @cached_method + def cograph_forest_edges(self): + r""" + Return the forest edges of the cograph of ``self``. + """ + if self._cograph_forest_edges is not None: + return self._cograph_forest_edges + cdef CMR_GRAPH *cograph = CMRseymourCograph(self._dec) + cdef size_t num_edges = CMRseymourCographSizeForest(self._dec) + cdef CMR_GRAPH_EDGE *edges = CMRseymourCographForest(self._dec) + cdef bool *arcs_reversed + base_ring = self.base_ring() + if base_ring.characteristic() == 2: + self._cograph_forest_edges = _sage_edges(cograph, edges, num_edges, self.row_keys()) + else: + arcs_reversed = CMRseymourCographArcsReversed(self._dec) + self._cograph_forest_edges = _sage_arcs(cograph, edges, arcs_reversed, num_edges, self.row_keys()) + return self._cograph_forest_edges + + @cached_method + def cograph_coforest_edges(self): + r""" + Return the forest edges of the cograph of ``self``. + """ + if self._cograph_coforest_edges is not None: + return self._cograph_coforest_edges + cdef CMR_GRAPH *cograph = CMRseymourCograph(self._dec) + cdef size_t num_edges = CMRseymourCographSizeCoforest(self._dec) + cdef CMR_GRAPH_EDGE *edges = CMRseymourCographCoforest(self._dec) + cdef bool *arcs_reversed + base_ring = self.base_ring() + if base_ring.characteristic() == 2: + self._cograph_coforest_edges = _sage_edges(cograph, edges, num_edges, self.column_keys()) + else: + arcs_reversed = CMRseymourCographArcsReversed(self._dec) + self._cograph_coforest_edges = _sage_arcs(cograph, edges, arcs_reversed, num_edges, self.column_keys()) + return self._cograph_coforest_edges + + +cdef class SeriesParallelReductionNode(DecompositionNode): + + def core(self): + r""" + Return the core of ``self``. + + A :class:`SeriesParallelReductionNode` indicates that `M` + arises from a smaller matrix `M'` (called the *core*) + by successively adding zero rows/columns, + unit rows/columns or duplicates of existing rows/columns + (potentially scaled with `-1`). + + Note that such series-parallel reductions preserve total unimodularity + and binary regularity. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: M = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 6, sparse=True), + ....: [[1, 1, 1, 1, 1, 0], [1, 1, 1, 0, 0, 0], + ....: [1, 0, 1, 1, 0, 1], [1, 0, 0, 1, 1, 0], + ....: [1, 1, 0, 0, 1, 0]]); M + [1 1 1 1 1 0] + [1 1 1 0 0 0] + [1 0 1 1 0 1] + [1 0 0 1 1 0] + [1 1 0 0 1 0] + sage: result, certificate = M.is_totally_unimodular(certificate=True) + sage: result, certificate + (True, SeriesParallelReductionNode (5×6)) + sage: certificate.core() + [1 1 1 1 1] + [1 1 1 0 0] + [1 0 1 1 0] + [1 0 0 1 1] + [1 1 0 0 1] + """ + return self.child_nodes()[0].matrix() + + +cdef class R10Node(DecompositionNode): + r""" + Special R10 Node. + Only two possible 5 by 5 matrices up to row and column permutations and negations. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: R10 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 5, sparse=True), + ....: [[1, 0, 0, 1, 1], + ....: [1, 1, 0, 0, 1], + ....: [0, 1, 1, 0, 1], + ....: [0, 0, 1, 1, 1], + ....: [1, 1, 1, 1, 1]]); R10 + [1 0 0 1 1] + [1 1 0 0 1] + [0 1 1 0 1] + [0 0 1 1 1] + [1 1 1 1 1] + sage: result, certificate = R10.is_totally_unimodular(certificate=True) + sage: result + True + sage: R10.is_network_matrix() + False + sage: R10.is_conetwork_matrix() + False + sage: result, certificate = R10._is_binary_linear_matroid_regular(certificate=True) + sage: result + True + sage: certificate._is_binary_linear_matroid_graphic() + False + sage: certificate._is_binary_linear_matroid_cographic() + False + + sage: R10 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 5, sparse=True), + ....: [[ 1,-1, 0, 0,-1], + ....: [-1, 1,-1, 0, 0], + ....: [ 0,-1, 1,-1, 0], + ....: [ 0, 0,-1, 1,-1], + ....: [-1, 0, 0,-1, 1]]); R10 + [ 1 -1 0 0 -1] + [-1 1 -1 0 0] + [ 0 -1 1 -1 0] + [ 0 0 -1 1 -1] + [-1 0 0 -1 1] + sage: result, certificate = R10.is_totally_unimodular(certificate=True) + sage: result + True + sage: R10.is_network_matrix() + False + sage: R10.is_conetwork_matrix() + False + + sage: R10 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 5, sparse=True), + ....: [[1, 1, 0, 0, 1], + ....: [1, 1,-1, 0, 0], + ....: [0, 1,-1,-1, 0], + ....: [0, 0, 1, 1, 1], + ....: [1, 0, 0, 1, 1]]); R10 + [ 1 1 0 0 1] + [ 1 1 -1 0 0] + [ 0 1 -1 -1 0] + [ 0 0 1 1 1] + [ 1 0 0 1 1] + sage: result, certificate = R10.is_totally_unimodular(certificate=True) + sage: result + True + sage: R10.is_network_matrix() + False + sage: R10.is_conetwork_matrix() + False + + sage: R10 = Matrix_cmr_chr_sparse(MatrixSpace(GF(2), 5, 5, sparse=True), + ....: [[1, 1, 0, 0, 1], + ....: [1, 1,-1, 0, 0], + ....: [0, 1,-1,-1, 0], + ....: [0, 0, 1, 1, 1], + ....: [1, 0, 0, 1, 1]]); R10 + [1 1 0 0 1] + [1 1 1 0 0] + [0 1 1 1 0] + [0 0 1 1 1] + [1 0 0 1 1] + sage: result, certificate = R10._is_binary_linear_matroid_regular(certificate=True) + sage: result + True + sage: certificate + R10Node (5×5) a reduced matrix representation of R10 matroid + sage: certificate._is_binary_linear_matroid_graphic() + False + sage: certificate._is_binary_linear_matroid_cographic() + False + """ + + @cached_method + def _matroid(self): + r""" + Return the R10 matroid represented by ``self``. + ``self.matrix()`` is the reduced matrix representation of the matroid. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: R10 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 5, sparse=True), + ....: [[1, 1, 1, 1, 1], + ....: [1, 1, 1, 0, 0], + ....: [1, 0, 1, 1, 0], + ....: [1, 0, 0, 1, 1], + ....: [1, 1, 0, 0, 1]]); R10 + [1 1 1 1 1] + [1 1 1 0 0] + [1 0 1 1 0] + [1 0 0 1 1] + [1 1 0 0 1] + sage: result, certificate = R10.is_totally_unimodular(certificate=True, + ....: row_keys=range(5), + ....: column_keys='abcde') + sage: certificate._matroid() + R10: Regular matroid of rank 5 on 10 elements with 162 bases + sage: certificate._isomorphism() + {'a': 0, + 'b': 1, + 'c': 2, + 'd': 'a', + 'e': 'b', + 'f': 4, + 'g': 'c', + 'h': 'e', + 'i': 3, + 'j': 'd'} + sage: result, certificate = R10.is_totally_unimodular(certificate=True) + sage: certificate._matroid() + R10: Regular matroid of rank 5 on 10 elements with 162 bases + sage: certificate._isomorphism() + {'a': 0, + 'b': 1, + 'c': 2, + 'd': 5, + 'e': 6, + 'f': 4, + 'g': 7, + 'h': 9, + 'i': 3, + 'j': 8} + + sage: R10 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 5, 5, sparse=True), + ....: [[ 1,-1, 0, 0,-1], + ....: [-1, 1,-1, 0, 0], + ....: [ 0,-1, 1,-1, 0], + ....: [ 0, 0,-1, 1,-1], + ....: [-1, 0, 0,-1, 1]]); R10 + [ 1 -1 0 0 -1] + [-1 1 -1 0 0] + [ 0 -1 1 -1 0] + [ 0 0 -1 1 -1] + [-1 0 0 -1 1] + sage: result, certificate = R10.is_totally_unimodular(certificate=True, + ....: row_keys=range(5), + ....: column_keys='abcde') + sage: certificate._matroid() + R10: Regular matroid of rank 5 on 10 elements with 162 bases + """ + from sage.matroids.constructor import Matroid + if self.row_keys() is None or self.column_keys() is None: + M = Matroid(reduced_matrix=self.matrix(), regular=True) + else: + M = Matroid(reduced_morphism=self.morphism(), regular=True) + M.rename("R10: " + repr(M)) + return M + + def _isomorphism(self): + r""" + Return one isomorphism between the R10 matroid + and ``self._matroid()``. + """ + import sage.matroids.matroids_catalog as matroids + M0 = matroids.catalog.R10() + result, isomorphism = M0.is_isomorphic(self._matroid(), certificate=True) + if result is False: + raise ValueError("This is not a R10 node") + else: + return isomorphism + + def _repr_(self): + r""" + Return a string representation of ``self``. + """ + result = super()._repr_() + result += f' a reduced matrix representation of R10 matroid' + return result + + +cdef class PivotsNode(DecompositionNode): + + def npivots(self): + r""" + Return the number of pivots in ``self``. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: R12 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 9, 12, sparse=True), + ....: [[ 1, -1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1], + ....: [ 0, 0, 0, 1, -1, 0, 0, 0, 1, 1, 1, 1], + ....: [ 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1], + ....: [ 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0], + ....: [ 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, -1, -1], + ....: [ 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0], + ....: [ 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, -1, -1], + ....: [ 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0], + ....: [ 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1]]) + sage: result, certificate = R12.is_totally_unimodular(certificate=True) + sage: certificate + PivotsNode (9×12) + sage: certificate.npivots() + 1 + """ + return CMRseymourNumPivots(self._dec) + + @cached_method + def pivot_rows_and_columns(self): + r""" + Return a tuple of the row and column indices of all pivot entries. + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: R12 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 9, 12, sparse=True), + ....: [[ 1, -1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1], + ....: [ 0, 0, 0, 1, -1, 0, 0, 0, 1, 1, 1, 1], + ....: [ 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1], + ....: [ 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0], + ....: [ 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, -1, -1], + ....: [ 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0], + ....: [ 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, -1, -1], + ....: [ 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0], + ....: [ 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1]]) + sage: result, certificate = R12.is_totally_unimodular(certificate=True) + sage: certificate + PivotsNode (9×12) + sage: certificate.pivot_rows_and_columns() + ((1, 8),) + """ + cdef size_t *pivot_rows = CMRseymourPivotRows(self._dec) + cdef size_t *pivot_columns = CMRseymourPivotColumns(self._dec) + + return tuple((pivot_rows[i], pivot_columns[i]) for i in range(self.npivots())) + + def _children(self): + r""" + Return a tuple of the tuples of children and their row and column keys. + The underlying implementation of :meth:`child_nodes` + and :meth:`child_indices`. + + If row and column keys are not given, set the default keys. + See alse :meth:set_default_keys + + EXAMPLES:: + + sage: from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + sage: R12 = Matrix_cmr_chr_sparse(MatrixSpace(ZZ, 9, 12, sparse=True), + ....: [[ 1, -1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1], + ....: [ 0, 0, 0, 1, -1, 0, 0, 0, 1, 1, 1, 1], + ....: [ 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1], + ....: [ 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0], + ....: [ 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, -1, -1], + ....: [ 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0], + ....: [ 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, -1, -1], + ....: [ 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0], + ....: [ 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1]]) + sage: result, certificate = R12.is_totally_unimodular(certificate=True) + sage: certificate + PivotsNode (9×12) + sage: certificate.row_keys() + sage: certificate.child_nodes() + (ThreeSumNode (9×12) with 2 children,) + sage: certificate.row_keys() + (r0, r1, r2, r3, r4, r5, r6, r7, r8) + """ + if self._child_nodes is not None: + return self._child_nodes + self.set_default_keys() + children_tuple = tuple(self._create_child_node(index) + for index in range(self.nchildren())) + self._child_nodes = children_tuple + return self._child_nodes + + +cdef class SymbolicNode(DecompositionNode): + + def __init__(self, symbol, *, row_keys=None, column_keys=None, base_ring=None): + r""" + EXAMPLES:: + + sage: from sage.matrix.seymour_decomposition import SymbolicNode + sage: X = SymbolicNode('X', row_keys='abc', column_keys=range(6)); X + SymbolicNode X (3×6) + sage: XX = X.one_sum(X) + Traceback (most recent call last): + ... + ValueError: keys must be disjoint... + sage: XX = X.one_sum(X, summand_ids=(0, 1)); XX + OneSumNode (6×12) with 2 children + sage: XX.row_keys() + ((0, 'a'), (0, 'b'), (0, 'c'), (1, 'a'), (1, 'b'), (1, 'c')) + sage: T = XX.as_ordered_tree(); T + OneSumNode (6×12) with 2 children[SymbolicNode X (3×6)[], + SymbolicNode X (3×6)[]] + sage: unicode_art(T) + ╭────────────────OneSumNode (6×12) with 2 children + │ │ + SymbolicNode X (3×6) SymbolicNode X (3×6) + sage: Y = SymbolicNode('Y', row_keys='de', column_keys='fg'); Y + SymbolicNode Y (2×2) + sage: XY = X.one_sum(Y); XY + OneSumNode (5×8) with 2 children + """ + super().__init__(row_keys=row_keys, column_keys=column_keys, base_ring=base_ring) + self._symbol = symbol + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLES:: + + sage: from sage.matrix.seymour_decomposition import SymbolicNode + sage: X = SymbolicNode('X', row_keys='abc', column_keys=range(6)) + sage: print(X) + SymbolicNode X (3×6) + """ + nrows, ncols = self.dimensions() + symbol = self.symbol() + return f'{self.__class__.__name__} {symbol} ({nrows}×{ncols})' + + def matrix(self): + raise ValueError('symbolic nodes are not backed by CMR matrices') + + def symbol(self): + return self._symbol + + +cdef class ElementKey: + + cdef frozenset _key + cdef bint _composition + + def __init__(self, keys, composition=False): + r""" + Create the element key for a row or column index + of :class:`DecompositionNode`. + + INPUT: + + - ``keys`` -- a row/column key or a tuple + (`\pm 1`, row/column key, `\pm 1`, row/column key). + + - ``composition`` -- ``True`` or ``False`` (default). + whether the key is a composition key or not. + If ``False``, ``self._key`` is a frozenset with a row/column key. + If ``True``, ``self._key`` is a frozenset with two tuples, + where each tuple has a sign value and a row/column key. + For example, ``frozenset((1,'a'), (-1,'7'))``. + """ + if composition: + sign1, key1, sign2, key2 = keys + self._key = frozenset([(sign1, key1), (sign2, key2)]) + self._composition = True + else: + self._key = frozenset((keys,)) + self._composition = False + + @property + def key(self): + return self._key + + def __hash__(self): + """ + Return a hash of this element key + """ + return hash(self._key) + + def __eq__(self, other): + if isinstance(other, ElementKey): + return self._key == other._key + return False + + def __repr__(self): + """ + Return a string representation of ``self``. + The composition key is sorted by the string of keys. + """ + if self._composition: + sorted_key = sorted(self._key, key=lambda x: (str(x[1]), x[0])) + return "".join(['+'+str(a[1]) if a[0] == 1 else '-'+str(a[1]) for a in sorted_key]) + else: + return "".join([str(a) for a in self._key]) + + +cdef _class(CMR_SEYMOUR_NODE *dec): + r""" + Return the class of the decomposition `CMR_SEYMOUR_NODE`. + + INPUT: + + - ``dec`` -- a ``CMR_SEYMOUR_NODE`` + """ + cdef CMR_SEYMOUR_NODE_TYPE typ = CMRseymourType(dec) + + if typ == CMR_SEYMOUR_NODE_TYPE_ONE_SUM: + return OneSumNode + if typ == CMR_SEYMOUR_NODE_TYPE_TWO_SUM: + return TwoSumNode + if typ == CMR_SEYMOUR_NODE_TYPE_THREE_SUM: + return ThreeSumNode + if typ == CMR_SEYMOUR_NODE_TYPE_GRAPH: + return GraphicNode + if typ == CMR_SEYMOUR_NODE_TYPE_COGRAPH: + return CographicNode + if typ == CMR_SEYMOUR_NODE_TYPE_PLANAR: + return PlanarNode + if typ == CMR_SEYMOUR_NODE_TYPE_SERIES_PARALLEL: + return SeriesParallelReductionNode + if typ == CMR_SEYMOUR_NODE_TYPE_PIVOTS: + return PivotsNode + if typ == CMR_SEYMOUR_NODE_TYPE_IRREGULAR: + return ThreeConnectedIrregularNode + if typ == CMR_SEYMOUR_NODE_TYPE_UNKNOWN: + return UnknownNode + if typ == CMR_SEYMOUR_NODE_TYPE_R10: + return R10Node + raise NotImplementedError + + +cdef create_DecompositionNode(CMR_SEYMOUR_NODE *dec, matrix=None, row_keys=None, column_keys=None, base_ring=None): + r""" + Create an instance of a subclass of :class:`DecompositionNode`. + + INPUT: + + - ``dec`` -- a ``CMR_SEYMOUR_NODE`` + """ + if dec == NULL: + return None + cdef DecompositionNode result = _class(dec)( + matrix, row_keys=row_keys, column_keys=column_keys, base_ring=base_ring) + result._set_dec(dec) + return result diff --git a/src/sage/matroids/graphic_matroid.pxd b/src/sage/matroids/graphic_matroid.pxd index 3684ab19f0d..b433d02dbd7 100644 --- a/src/sage/matroids/graphic_matroid.pxd +++ b/src/sage/matroids/graphic_matroid.pxd @@ -22,8 +22,8 @@ cdef class GraphicMatroid(Matroid): cpdef _is_isomorphic(self, other, certificate=*) cpdef _isomorphism(self, other) cpdef bint is_valid(self) noexcept - cpdef bint is_graphic(self) noexcept - cpdef bint is_regular(self) noexcept + cpdef bint is_graphic(self, algorithm=*) except -1 + cpdef bint is_regular(self, algorithm=*) except -1 cpdef graph(self) cpdef vertex_map(self) cpdef list groundset_to_edges(self, X) diff --git a/src/sage/matroids/graphic_matroid.pyx b/src/sage/matroids/graphic_matroid.pyx index 5e740e78637..f2c7c32987c 100644 --- a/src/sage/matroids/graphic_matroid.pyx +++ b/src/sage/matroids/graphic_matroid.pyx @@ -1110,7 +1110,7 @@ cdef class GraphicMatroid(Matroid): """ return True - cpdef bint is_graphic(self) noexcept: + cpdef bint is_graphic(self, algorithm=None) except -1: r""" Return if ``self`` is graphic. @@ -1124,7 +1124,7 @@ cdef class GraphicMatroid(Matroid): """ return True - cpdef bint is_regular(self) noexcept: + cpdef bint is_regular(self, algorithm=None) except -1: r""" Return if ``self`` is regular. diff --git a/src/sage/matroids/linear_matroid.pxd b/src/sage/matroids/linear_matroid.pxd index b0890a76148..8ae64598d99 100644 --- a/src/sage/matroids/linear_matroid.pxd +++ b/src/sage/matroids/linear_matroid.pxd @@ -91,7 +91,9 @@ cdef class BinaryMatroid(LinearMatroid): cpdef _fast_isom_test(self, other) cpdef relabel(self, mapping) - cpdef bint is_graphic(self) noexcept + cpdef bint is_graphic(self, algorithm=*) except -1 + cpdef _is_graphic_GG(self) + cpdef _is_graphic_cmr(self) cpdef bint is_valid(self) noexcept @@ -172,6 +174,6 @@ cdef class RegularMatroid(LinearMatroid): cpdef has_line_minor(self, k, hyperlines=*, certificate=*) cpdef _linear_extension_chains(self, F, fundamentals=*) - cpdef bint is_regular(self) noexcept - cpdef bint is_graphic(self) noexcept + cpdef bint is_regular(self, algorithm=*) except -1 + cpdef bint is_graphic(self, algorithm=*) except -1 cpdef bint is_valid(self) noexcept diff --git a/src/sage/matroids/linear_matroid.pyx b/src/sage/matroids/linear_matroid.pyx index 6848604f0c8..96931539ef7 100644 --- a/src/sage/matroids/linear_matroid.pyx +++ b/src/sage/matroids/linear_matroid.pyx @@ -3793,8 +3793,61 @@ cdef class BinaryMatroid(LinearMatroid): keep_initial_representation=False) # graphicness test - cpdef bint is_graphic(self) noexcept: + cpdef bint is_graphic(self, algorithm=None) except -1: + r""" + Test if the binary matroid is graphic. + + A matroid is *graphic* if there exists a graph whose edge set equals + the groundset of the matroid, such that a subset of elements of the + matroid is independent if and only if the corresponding subgraph is + acyclic. + + INPUT: + + - ``algorithm`` -- (default: ``None``); specify which algorithm + to check graphicness: + + - ``None`` -- an algorithm based on [GG2012]_. + - ``"cmr"`` -- an algorithm based on [BW1988b]_; + the optional package "cmr" is required. + + OUTPUT: + + Boolean. + + .. SEEALSO:: + + :meth:`M.is_graphic() ` + :meth:`M.is_graphic() ` + + EXAMPLES:: + + sage: R10 = matroids.catalog.R10() + sage: M = Matroid(ring=GF(2), reduced_matrix=R10.representation( + ....: reduced=True, labels=False)) + sage: M.is_graphic(algorithm="cmr") # optional - cmr + False + sage: K5 = Matroid(graphs.CompleteGraph(5)) # needs sage.graphs + sage: K5.is_graphic(algorithm="cmr") # optional - cmr # needs sage.graphs + True + sage: K5 = Matroid(graphs.CompleteGraph(5), regular=True) # needs sage.graphs + sage: M = Matroid(ring=GF(2), reduced_matrix=K5.representation( # needs sage.graphs sage.rings.finite_rings + ....: reduced=True, labels=False)) + sage: M.is_graphic(algorithm="cmr") # optional - cmr # needs sage.graphs sage.rings.finite_rings + True + sage: M.dual().is_graphic(algorithm="cmr") # optional - cmr # needs sage.graphs + False """ + if algorithm is None: + return self._is_graphic_GG() + if algorithm == "cmr": + return self._is_graphic_cmr() + raise ValueError("Not a valid algorithm.") + + cpdef _is_graphic_GG(self): + r""" Test if the binary matroid is graphic. A matroid is *graphic* if there exists a graph whose edge set equals @@ -3809,14 +3862,14 @@ cdef class BinaryMatroid(LinearMatroid): sage: R10 = matroids.catalog.R10() sage: M = Matroid(ring=GF(2), reduced_matrix=R10.representation( ....: reduced=True, labels=False)) - sage: M.is_graphic() + sage: M._is_graphic_GG() False sage: K5 = Matroid(graphs.CompleteGraph(5), regular=True) # needs sage.graphs sage: M = Matroid(ring=GF(2), reduced_matrix=K5.representation( # needs sage.graphs sage.rings.finite_rings ....: reduced=True, labels=False)) - sage: M.is_graphic() # needs sage.graphs sage.rings.finite_rings + sage: M._is_graphic_GG() # needs sage.graphs sage.rings.finite_rings True - sage: M.dual().is_graphic() # needs sage.graphs + sage: M.dual()._is_graphic_GG() # needs sage.graphs False ALGORITHM: @@ -3862,6 +3915,51 @@ cdef class BinaryMatroid(LinearMatroid): # now self is graphic iff there is a binary vector x so that M*x = 0 and x_0 = 1, so: return BinaryMatroid(m).corank(frozenset([0])) > 0 + cpdef _is_graphic_cmr(self): + r""" + Test if the binary matroid is graphic. + + A matroid is *graphic* if there exists a graph whose edge set equals + the groundset of the matroid, such that a subset of elements of the + matroid is independent if and only if the corresponding subgraph is + acyclic. + + OUTPUT: + + Boolean. + + .. SEEALSO:: + + :meth:`M._is_binary_linear_matroid_graphic() ` + :meth:`M.is_network_matrix() ` + + EXAMPLES:: + + sage: R10 = matroids.catalog.R10() + sage: M = Matroid(ring=GF(2), reduced_matrix=R10.representation( + ....: reduced=True, labels=False)) + sage: M._is_graphic_cmr() # optional - cmr + False + sage: K5 = Matroid(graphs.CompleteGraph(5), regular=True) # needs sage.graphs + sage: M = Matroid(ring=GF(2), reduced_matrix=K5.representation( # needs sage.graphs sage.rings.finite_rings + ....: reduced=True, labels=False)) + sage: M._is_graphic_cmr() # optional - cmr # needs sage.graphs sage.rings.finite_rings + True + sage: M.dual()._is_graphic_cmr() # optional - cmr # needs sage.graphs + False + + ALGORITHM: + + The implemented recognition algorithm is based on [BW1988b]_, [An Almost Linear-Time Algorithm for Graph Realization](https://doi.org/10.1287/moor.13.1.99) by Robert E. Bixby and Donald K. Wagner (Mathematics of Operations Research, 1988). + For a matrix `M \in \{0,1\}^{m \times n}` with `k` nonzeros it runs in `\mathcal{O}( k \cdot \alpha(k, m) )` time, where `\alpha(\cdot)` denotes the inverse Ackerman function. + """ + from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + A = self.representation() + A_cmr = Matrix_cmr_chr_sparse(A.parent(), A) + return A_cmr._is_binary_linear_matroid_graphic() + cpdef bint is_valid(self) noexcept: r""" Test if the data obey the matroid axioms. @@ -6239,7 +6337,7 @@ cdef class RegularMatroid(LinearMatroid): fundamentals = set([1]) return LinearMatroid._linear_extension_chains(self, F, fundamentals) - cpdef bint is_graphic(self) noexcept: + cpdef bint is_graphic(self, algorithm=None) except -1: """ Test if the regular matroid is graphic. @@ -6248,27 +6346,36 @@ cdef class RegularMatroid(LinearMatroid): matroid is independent if and only if the corresponding subgraph is acyclic. + INPUT: + + - ``algorithm`` -- (default: ``None``); specify which algorithm + to check graphicness: + + - ``None`` -- an algorithm based on [GG2012]_. + - ``"cmr"`` -- an algorithm based on [BW1988b]_; + the optional package "cmr" is required. + OUTPUT: boolean + .. SEEALSO:: + + :meth:`M.is_graphic() ` + :meth:`M.is_graphic() ` + EXAMPLES:: sage: M = matroids.catalog.R10() - sage: M.is_graphic() + sage: M.is_graphic(algorithm="cmr") # optional - cmr False sage: M = Matroid(graphs.CompleteGraph(5), regular=True) # needs sage.graphs - sage: M.is_graphic() # needs sage.graphs sage.rings.finite_rings + sage: M.is_graphic(algorithm="cmr") # optional - cmr # needs sage.graphs sage.rings.finite_rings True - sage: M.dual().is_graphic() # needs sage.graphs + sage: M.dual().is_graphic(algorithm="cmr") # optional - cmr # needs sage.graphs False - - ALGORITHM: - - In a recent paper, Geelen and Gerards [GG2012]_ reduced the problem to - testing if a system of linear equations has a solution. While not the - fastest method, and not necessarily constructive (in the presence of - 2-separations especially), it is easy to implement. """ - return BinaryMatroid(reduced_matrix=self._reduced_representation()).is_graphic() + return BinaryMatroid(reduced_matrix=self._reduced_representation()).is_graphic(algorithm=algorithm) cpdef bint is_valid(self) noexcept: r""" @@ -6299,7 +6406,7 @@ cdef class RegularMatroid(LinearMatroid): # representation - cpdef bint is_regular(self) noexcept: + cpdef bint is_regular(self, algorithm=None) except -1: r""" Return if ``self`` is regular. diff --git a/src/sage/matroids/matroid.pxd b/src/sage/matroids/matroid.pxd index 6218cb804f4..d97883ed631 100644 --- a/src/sage/matroids/matroid.pxd +++ b/src/sage/matroids/matroid.pxd @@ -195,8 +195,8 @@ cdef class Matroid(SageObject): cpdef _local_ternary_matroid(self, basis=*) cpdef ternary_matroid(self, randomized_tests=*, verify=*) cpdef is_ternary(self, randomized_tests=*) - cpdef bint is_regular(self) noexcept - cpdef bint is_graphic(self) noexcept + cpdef bint is_regular(self, algorithm=*) except -1 + cpdef bint is_graphic(self, algorithm=*) except -1 # matroid k-closed cpdef is_k_closed(self, int k) diff --git a/src/sage/matroids/matroid.pyx b/src/sage/matroids/matroid.pyx index 0384ff81a22..358a9b1fc7d 100644 --- a/src/sage/matroids/matroid.pyx +++ b/src/sage/matroids/matroid.pyx @@ -305,6 +305,7 @@ REFERENCES ========== - [BC1977]_ +- [BW1988b]_ - [Cun1986]_ - [CMO2011]_ - [CMO2012]_ @@ -6578,14 +6579,36 @@ cdef class Matroid(SageObject): """ return self.ternary_matroid(randomized_tests=randomized_tests, verify=True) is not None - cpdef bint is_graphic(self) noexcept: + cpdef bint is_graphic(self, algorithm=None) except -1: r""" Return if ``self`` is graphic. + A matroid is *graphic* if there exists a graph whose edge set equals + the groundset of the matroid, such that a subset of elements of the + matroid is independent if and only if the corresponding subgraph is + acyclic. A matroid is graphic if and only if it has no minor isomorphic to any of the matroids `U_{2, 4}`, `F_7`, `F_7^*`, `M^*(K_5)`, and `M^*(K_{3, 3})`. + INPUT: + + - ``algorithm`` -- (default: ``None``); specify which algorithm + to check graphicness: + + - ``None`` -- an algorithm based on excluded minors. + - ``"cmr"`` -- an algorithm based on [BW1988b]_, + the optional package "cmr" is required. + + .. SEEALSO:: + + :meth:`M._is_graphic_cmr() ` + :meth:`M.is_graphic() ` + :meth:`M.is_graphic() ` + EXAMPLES:: sage: M = matroids.catalog.Wheel4() @@ -6594,25 +6617,38 @@ cdef class Matroid(SageObject): sage: M = matroids.catalog.U24() sage: M.is_graphic() False + sage: M = matroids.catalog.Wheel4() + sage: M.is_graphic(algorithm="cmr") # optional - cmr + True + sage: M = matroids.catalog.U24() + sage: M.is_graphic(algorithm="cmr") # optional - cmr + False REFERENCES: [Oxl2011]_, p. 385. """ - from sage.matroids.database_matroids import ( - U24, - Fano, - FanoDual, - K5dual, - K33dual - ) - excluded_minors = [U24(), Fano(), FanoDual(), K5dual(), K33dual()] - for M in excluded_minors: - if self.has_minor(M): - return False - return True + M = self.binary_matroid() + if M is None: # equivalent to checking for a U24 minor + return False + if algorithm is None: + from sage.matroids.database_matroids import ( + U24, + Fano, + FanoDual, + K5dual, + K33dual + ) + excluded_minors = [U24(), Fano(), FanoDual(), K5dual(), K33dual()] + for M in excluded_minors: + if self.has_minor(M): + return False + return True + if algorithm == "cmr": + return M._is_graphic_cmr() + raise ValueError("Not a valid algorithm.") - cpdef bint is_regular(self) noexcept: + cpdef bint is_regular(self, algorithm=None) except -1: r""" Return if ``self`` is regular. @@ -6623,25 +6659,57 @@ cdef class Matroid(SageObject): Alternatively, a matroid is regular if and only if it has no minor isomorphic to `U_{2, 4}`, `F_7`, or `F_7^*`. + INPUT: + + - ``algorithm`` -- (default: ``None``); specify which algorithm + to check regularity: + + - ``None`` -- an algorithm based on excluded minors. + - ``"cmr"`` -- an algorithm based on Seymour's decomposition, + the optional package "cmr" is required. + + .. SEEALSO:: + + :meth:`M.is_regular() ` + :meth:`M.is_regular() ` + :meth:`M._is_binary_linear_matroid_regular() ` + :meth:`M.is_totally_unimodular() ` + EXAMPLES:: sage: M = matroids.catalog.Wheel4() sage: M.is_regular() True sage: M = matroids.catalog.R9() - sage: M.is_regular() + sage: M.is_regular(algorithm="cmr") # optional - cmr + False + sage: from sage.matroids.advanced import LinearMatroid + sage: M1 = LinearMatroid(Matrix(ZZ,[[1,0,1,1],[0,1,1,-1]])) + sage: M1.is_regular(algorithm="cmr") # optional - cmr False REFERENCES: - [Oxl2011]_, p. 373. + [Oxl2011]_, p. 373, chapter 13. """ - if not self.is_binary(): # equivalent to checking for a U24 minor + M = self.binary_matroid() + if M is None: # equivalent to checking for a U24 minor return False - from sage.matroids.database_matroids import Fano, FanoDual - if self.has_minor(Fano()) or self.has_minor(FanoDual()): - return False - return True + if algorithm is None: + from sage.matroids.database_matroids import Fano, FanoDual + if self.has_minor(Fano()) or self.has_minor(FanoDual()): + return False + return True + if algorithm == "cmr": + from sage.matrix.matrix_cmr_sparse import Matrix_cmr_chr_sparse + A = M.representation() + A_cmr = Matrix_cmr_chr_sparse(A.parent(), A) + return A_cmr._is_binary_linear_matroid_regular() + raise ValueError("Not a valid algorithm.") # matroid k-closed diff --git a/src/sage/misc/sagedoc.py b/src/sage/misc/sagedoc.py index 827fd77e811..9b377921708 100644 --- a/src/sage/misc/sagedoc.py +++ b/src/sage/misc/sagedoc.py @@ -516,6 +516,7 @@ def process_dollars(s): 'doi': ('https://doi.org/%s', 'doi:%s'), 'pari': ('https://pari.math.u-bordeaux.fr/dochtml/help/%s', 'pari:%s'), 'mathscinet': ('https://www.ams.org/mathscinet-getitem?mr=%s', 'MathSciNet %s'), + 'cmr': ('https://discopt.github.io/cmr/%s.html', 'CMR: %s'), 'common_lisp': ('https://www.lispworks.com/documentation/lw50/CLHS/Body/%s.htm', 'Common Lisp: %s'), 'ecl': ('https://ecl.common-lisp.dev/static/manual/%s.html', 'ECL: %s'), 'gap': ('https://docs.gap-system.org/doc/ref/%s_mj.html', 'GAP: %s'), diff --git a/src/setup.py b/src/setup.py index dfa2e59b2e1..a10e7a59b4f 100755 --- a/src/setup.py +++ b/src/setup.py @@ -77,7 +77,7 @@ log.info("Discovering Python/Cython source code...") optional_packages = ['mcqd', 'bliss', 'tdlib', - 'coxeter3', 'sirocco', 'meataxe'] + 'coxeter3', 'sirocco', 'meataxe', 'cmr'] distributions_to_exclude = [f"sagemath-{pkg}" for pkg in optional_packages] files_to_exclude = filter_cython_sources(SAGE_SRC, distributions_to_exclude)