diff --git a/.gitignore b/.gitignore index 5edef4a5619..b209681b9b5 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,10 @@ $RECYCLE.BIN/ # SublimeText *.sublime-workspace + +################# +# SageMathCloud # +################# +*.sage-chat +*.sage-history +*.syncdoc* \ No newline at end of file diff --git a/COPYING.txt b/COPYING.txt index 6996709bd6a..0bd95adb8c1 100644 --- a/COPYING.txt +++ b/COPYING.txt @@ -76,6 +76,7 @@ linbox LGPLv2.1+ lrcalc GPLv2+ m4ri GPLv2+ m4rie GPLv2+ +markupsafe Simplified BSD mathjax Apache License 2.0 matplotlib Matplotlib License (BSD compatible, see below) maxima See below diff --git a/VERSION.txt b/VERSION.txt index 05177f694e4..f830b82a3c6 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -Sage version 6.6.beta5, released 2015-03-13 +Sage version 6.6.beta6, released 2015-03-19 diff --git a/build/deps b/build/deps index 6ec4b9ce142..5c77dfe4468 100644 --- a/build/deps +++ b/build/deps @@ -68,6 +68,7 @@ all-sage: \ $(INST)/$(LINBOX) \ $(INST)/$(M4RI) \ $(INST)/$(M4RIE) \ + $(INST)/$(MARKUPSAFE) \ $(INST)/$(MATHJAX) \ $(INST)/$(MATPLOTLIB) \ $(INST)/$(MAXIMA) \ @@ -467,7 +468,10 @@ $(INST)/$(SPHINX): $(INST)/$(PYTHON) $(INST)/$(SETUPTOOLS) $(INST)/$(DOCUTILS) \ $(INST)/$(JINJA2) $(INST)/$(PYGMENTS) +$(PIPE) "$(SAGE_SPKG) $(SPHINX) 2>&1" "tee -a $(SAGE_LOGS)/$(SPHINX).log" -$(INST)/$(JINJA2): $(INST)/$(PYTHON) $(INST)/$(SETUPTOOLS) $(INST)/$(DOCUTILS) +$(INST)/$(MARKUPSAFE): $(INST)/$(PYTHON) $(INST)/$(SETUPTOOLS) + +$(PIPE) "$(SAGE_SPKG) $(MARKUPSAFE) 2>&1" "tee -a $(SAGE_LOGS)/$(MARKUPSAFE).log" + +$(INST)/$(JINJA2): $(INST)/$(MARKUPSAFE) $(INST)/$(SPHINX) +$(PIPE) "$(SAGE_SPKG) $(JINJA2) 2>&1" "tee -a $(SAGE_LOGS)/$(JINJA2).log" $(INST)/$(PYGMENTS): $(INST)/$(PYTHON) $(INST)/$(SETUPTOOLS) diff --git a/build/install b/build/install index 84d4dfc4714..d9b1b77220b 100755 --- a/build/install +++ b/build/install @@ -343,6 +343,7 @@ LIBPNG=`newest_version libpng` LINBOX=`newest_version linbox` M4RI=`newest_version m4ri` M4RIE=`newest_version m4rie` +MARKUPSAFE=`newest_version markupsafe` MATHJAX=`newest_version mathjax` MATPLOTLIB=`newest_version matplotlib` MAXIMA=`newest_version maxima` diff --git a/build/pkgs/configure/checksums.ini b/build/pkgs/configure/checksums.ini index 86140368451..27be31a7155 100644 --- a/build/pkgs/configure/checksums.ini +++ b/build/pkgs/configure/checksums.ini @@ -1,4 +1,4 @@ tarball=configure-VERSION.tar.gz -sha1=12e8d35579711c8bf8beddb890aaebd6b2c51db1 -md5=dbb592c16e66f4f2e3345071fb115abf -cksum=693230472 +sha1=89ece2f5d378cda9468a3437854b10809d1e57c3 +md5=6bf787d800c20ead4a74e5ff00b1c9c8 +cksum=913450101 diff --git a/build/pkgs/configure/package-version.txt b/build/pkgs/configure/package-version.txt index 78eb67cee1a..dd475631bae 100644 --- a/build/pkgs/configure/package-version.txt +++ b/build/pkgs/configure/package-version.txt @@ -1 +1 @@ -75 +76 diff --git a/build/pkgs/database_pari/SPKG.txt b/build/pkgs/database_pari/SPKG.txt new file mode 100644 index 00000000000..fe264e9ca9c --- /dev/null +++ b/build/pkgs/database_pari/SPKG.txt @@ -0,0 +1,29 @@ += database_pari = + +== Description == + +The collection of optional PARI packages elldata, seadata, galpol, +nftables (galdata is included in the standard PARI spkg). +See http://pari.math.u-bordeaux.fr/packages.html + +== License == + +GNU General Public License (GPL version 2 or any later version). + +== SPKG Maintainers == + +* Jeroen Demeyer + +== Upstream Contact == + +http://pari.math.u-bordeaux.fr/ + +== Dependencies == + +* Installation: None +* Runtime: PARI/GP + +== Special Update/Build Instructions == + +Download the four mentioned tarballs and extract them, then move them +out of the top-level directory data. diff --git a/build/pkgs/database_pari/checksums.ini b/build/pkgs/database_pari/checksums.ini new file mode 100644 index 00000000000..59dd77df74b --- /dev/null +++ b/build/pkgs/database_pari/checksums.ini @@ -0,0 +1,4 @@ +tarball=database_pari-VERSION.tar.bz2 +sha1=92dcb68e7a6def53ffc5fb82e3593d5b80c940cb +md5=652d36d18ea300193957120815298be7 +cksum=2257060384 diff --git a/build/pkgs/database_pari/package-version.txt b/build/pkgs/database_pari/package-version.txt new file mode 100644 index 00000000000..bc51480f53c --- /dev/null +++ b/build/pkgs/database_pari/package-version.txt @@ -0,0 +1 @@ +20140908 diff --git a/build/pkgs/database_pari/spkg-check b/build/pkgs/database_pari/spkg-check new file mode 100755 index 00000000000..dde57035983 --- /dev/null +++ b/build/pkgs/database_pari/spkg-check @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +if [ -z "$SAGE_SRC" ]; then + echo >&2 "SAGE_SRC undefined ... exiting" + echo >&2 "Maybe run 'sage --sh'?" + exit 1 +fi + +sage -tp --long --optional=database_pari,sage "$SAGE_SRC/sage/tests/parigp.py" "$SAGE_SRC/sage/libs/pari" diff --git a/build/pkgs/database_pari/spkg-install b/build/pkgs/database_pari/spkg-install new file mode 100755 index 00000000000..d28a48d3bc4 --- /dev/null +++ b/build/pkgs/database_pari/spkg-install @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +if [ -z "$GP_DATA_DIR" ]; then + echo >&2 "GP_DATA_DIR undefined ... exiting" + echo >&2 "Maybe run 'sage --sh'?" + exit 1 +fi + +cd src +cp -pR elldata galpol nftables seadata "$GP_DATA_DIR" diff --git a/build/pkgs/eclib/checksums.ini b/build/pkgs/eclib/checksums.ini index 630c07f14e3..5fe0101d2e0 100644 --- a/build/pkgs/eclib/checksums.ini +++ b/build/pkgs/eclib/checksums.ini @@ -1,4 +1,4 @@ tarball=eclib-VERSION.tar.bz2 -sha1=b8a8b5232caf2332e650d8b6b8ec009979312974 -md5=da8b822c6bbef913bcef6d7491c8c999 -cksum=3800001876 +sha1=208567933690c8805eec313cfdedf7f9b9475ddb +md5=2ebc69ef8d99750e91178b8eb8f26c9d +cksum=3283693850 diff --git a/build/pkgs/eclib/package-version.txt b/build/pkgs/eclib/package-version.txt index cb7b2511ca6..d700bef6991 100644 --- a/build/pkgs/eclib/package-version.txt +++ b/build/pkgs/eclib/package-version.txt @@ -1 +1 @@ -20140921 +20150228 diff --git a/build/pkgs/jinja2/checksums.ini b/build/pkgs/jinja2/checksums.ini index 4e74f2820bd..1f3635ea5e8 100644 --- a/build/pkgs/jinja2/checksums.ini +++ b/build/pkgs/jinja2/checksums.ini @@ -1,4 +1,4 @@ -tarball=jinja2-VERSION.tar.bz2 -sha1=e9e13b4f573effd35e03b1ecd978c684342d549c -md5=3ca3504c27a147d38f4d32214a3f3004 -cksum=1443664485 +tarball=jinja2-VERSION.tar.gz +sha1=25ab3881f0c1adfcf79053b58de829c5ae65d3ac +md5=b9dffd2f3b43d673802fe857c8445b1a +cksum=3563905877 diff --git a/build/pkgs/jinja2/package-version.txt b/build/pkgs/jinja2/package-version.txt index 0cadbc1e33b..2c9b4ef42ec 100644 --- a/build/pkgs/jinja2/package-version.txt +++ b/build/pkgs/jinja2/package-version.txt @@ -1 +1 @@ -2.5.5 +2.7.3 diff --git a/build/pkgs/latte_int/checksums.ini b/build/pkgs/latte_int/checksums.ini index 72e4b5d312a..0ac787821e1 100644 --- a/build/pkgs/latte_int/checksums.ini +++ b/build/pkgs/latte_int/checksums.ini @@ -1,4 +1,4 @@ -tarball=latte_int-VERSION.tar.gz -sha1=165c9173b13f4bc9cb825ef63c06d6fb20dc41b5 -md5=57b151f7bb49fe5154a5697b70d359f9 -cksum=1072837189 +tarball=latte_int-VERSION.tar.bz2 +sha1=1a1b475d69697c45218b133df9b0519894ebce28 +md5=3a862e6a5d1b2e10b0bee342fc0f27a9 +cksum=3434585848 diff --git a/build/pkgs/markupsafe/SPKG.txt b/build/pkgs/markupsafe/SPKG.txt new file mode 100644 index 00000000000..e0b5a22e19f --- /dev/null +++ b/build/pkgs/markupsafe/SPKG.txt @@ -0,0 +1,19 @@ += markupsafe = + +== Description == + +Implements a XML/HTML/XHTML Markup safe string for Python + +== License == + +Simplified BSD + +== Upstream Contact == + +Home page: http://github.com/mitsuhiko/markupsafe + +== Dependencies == + +Python, setuptools + + diff --git a/build/pkgs/markupsafe/checksums.ini b/build/pkgs/markupsafe/checksums.ini new file mode 100644 index 00000000000..d09e23b7503 --- /dev/null +++ b/build/pkgs/markupsafe/checksums.ini @@ -0,0 +1,4 @@ +tarball=markupsafe-VERSION.tar.gz +sha1=cd5c22acf6dd69046d6cb6a3920d84ea66bdf62a +md5=f5ab3deee4c37cd6a922fb81e730da6e +cksum=586757310 diff --git a/build/pkgs/markupsafe/package-version.txt b/build/pkgs/markupsafe/package-version.txt new file mode 100644 index 00000000000..39010d220e8 --- /dev/null +++ b/build/pkgs/markupsafe/package-version.txt @@ -0,0 +1 @@ +0.23 diff --git a/build/pkgs/markupsafe/spkg-install b/build/pkgs/markupsafe/spkg-install new file mode 100755 index 00000000000..f0ac21a8211 --- /dev/null +++ b/build/pkgs/markupsafe/spkg-install @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +if [ -z "$SAGE_LOCAL" ]; then + echo >&2 "SAGE_LOCAL undefined ... exiting" + echo >&2 "Maybe run 'sage --sh'?" + exit 1 +fi + +cd src + +python setup.py install +if [ $? -ne 0 ]; then + echo "Error installing markupsafe ... exiting" + exit 1 +fi diff --git a/build/pkgs/modular_decomposition/SPKG.txt b/build/pkgs/modular_decomposition/SPKG.txt new file mode 100644 index 00000000000..277eeded0c5 --- /dev/null +++ b/build/pkgs/modular_decomposition/SPKG.txt @@ -0,0 +1,29 @@ += modular decomposition = + +== Description == + +This is an implementation of a modular decomposition algorithm. + +http://www.liafa.jussieu.fr/~fm/ (in french) + +== License == + +GPL + +== SPKG Maintainers == + +Nathann Cohen (nathann.cohen@gmail.com) + +== Upstream Contact == + +Fabien de Montgolfier + +http://www.liafa.jussieu.fr/~fm/ + +== Dependencies == + +None + +== Patches == + +None diff --git a/build/pkgs/modular_decomposition/checksums.ini b/build/pkgs/modular_decomposition/checksums.ini new file mode 100644 index 00000000000..2c97fe5389a --- /dev/null +++ b/build/pkgs/modular_decomposition/checksums.ini @@ -0,0 +1,4 @@ +tarball=modular_decomposition-VERSION.tar.bz2 +sha1=b0ce6d839d1cd2e93d806e70b13bc40bcdbaf9e9 +md5=9bc5245c5fab9df4f45c8e10c27cf3b8 +cksum=2034006428 diff --git a/build/pkgs/modular_decomposition/package-version.txt b/build/pkgs/modular_decomposition/package-version.txt new file mode 100644 index 00000000000..b3089db1733 --- /dev/null +++ b/build/pkgs/modular_decomposition/package-version.txt @@ -0,0 +1 @@ +20100607 diff --git a/build/pkgs/modular_decomposition/spkg-install b/build/pkgs/modular_decomposition/spkg-install new file mode 100755 index 00000000000..e87a40528fd --- /dev/null +++ b/build/pkgs/modular_decomposition/spkg-install @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +if [ "$SAGE_LOCAL" = "" ]; then + echo "SAGE_LOCAL undefined ... exiting"; + echo "Maybe run 'sage -sh'?" + exit 1 +fi + +cd "src" + +gcc -o "$SAGE_LOCAL/lib/libmodulardecomposition.so" dm.c random.c -fPIC --shared && +mv dm_english.h "$SAGE_LOCAL/include/modular_decomposition.h" + +if [ $? -ne 0 ]; then + echo "An error occurred whilst building modular_decomposition" + exit 1 +fi diff --git a/build/pkgs/pari/checksums.ini b/build/pkgs/pari/checksums.ini index e1254b553cd..30e411f46d1 100644 --- a/build/pkgs/pari/checksums.ini +++ b/build/pkgs/pari/checksums.ini @@ -1,4 +1,4 @@ tarball=pari-VERSION.tar.gz -sha1=c11c4452abfaf1a8f1498e4d0ae83995d2cc1f6e -md5=7bb2efb14829832627bc81735bc50db5 -cksum=2193237587 +sha1=7617f60af26582d4a77e5af413b26fde32bd4f1a +md5=2ae5684a10d557016fc1b6ad10b8bf80 +cksum=4177496658 diff --git a/build/pkgs/pari/package-version.txt b/build/pkgs/pari/package-version.txt index 3a769906b9d..4846e2756d7 100644 --- a/build/pkgs/pari/package-version.txt +++ b/build/pkgs/pari/package-version.txt @@ -1 +1 @@ -2.8-1315-g031c9d6 +2.8-1369-g0e48e9b diff --git a/build/pkgs/pari/spkg-install b/build/pkgs/pari/spkg-install index 7d32abb68c3..6a7b1ebc9d3 100755 --- a/build/pkgs/pari/spkg-install +++ b/build/pkgs/pari/spkg-install @@ -17,7 +17,9 @@ TOP=`pwd` ####################################################################### cd src for patch in ../patches/*.patch; do - patch -p1 <"$patch" + # Use --no-backup-if-mismatch to prevent .orig files (these confuse + # PARI's Perl script which creates pari.desc). + patch --no-backup-if-mismatch -p1 <"$patch" if [ $? -ne 0 ]; then echo >&2 "Error applying '$patch'" exit 1 diff --git a/build/pkgs/sagenb/spkg-install b/build/pkgs/sagenb/spkg-install index 65865a93f89..84127395b86 100755 --- a/build/pkgs/sagenb/spkg-install +++ b/build/pkgs/sagenb/spkg-install @@ -46,3 +46,12 @@ PKG=$(ls -1 src | GREP_OPTIONS= grep sagenb-) # Install sagenb into site-packages easy_install -H None "src/$PKG" || die "Error installing sagenb !" + +# let sagenb use mathjax spkg +SAGENB_VERSION=$(cat ${SAGE_ROOT}/build/pkgs/sagenb/package-version.txt) +PYTHON_MAJOR_VERSION=$(cat ${SAGE_ROOT}/build/pkgs/python/package-version.txt | sed 's/\.[^.]*$//g') +cd ${SAGE_LOCAL}/lib/python/site-packages/sagenb-${SAGENB_VERSION}-py${PYTHON_MAJOR_VERSION}.egg/sagenb/data +# the following line can be removed once sagenb does not ship mathjax anymore. +rm -rf mathjax +ln -s ../../../../../../share/mathjax/ + diff --git a/src/bin/sage-CSI b/src/bin/sage-CSI index f07e0969dd0..658f7fe8cee 100755 --- a/src/bin/sage-CSI +++ b/src/bin/sage-CSI @@ -1,5 +1,7 @@ #!/usr/bin/env python +from __future__ import print_function + description = """ Attach the debugger to a Python process (given by its pid) and extract as much information about its internal state as possible @@ -12,8 +14,6 @@ description = """ # set) are automatically deleted, but with a negative value they are # never deleted. -from __future__ import print_function - import sys import os import subprocess diff --git a/src/bin/sage-banner b/src/bin/sage-banner index 7105eb2b386..f34d3d4258a 100644 --- a/src/bin/sage-banner +++ b/src/bin/sage-banner @@ -1,5 +1,5 @@ ┌────────────────────────────────────────────────────────────────────┐ -│ Sage Version 6.6.beta5, Release Date: 2015-03-13 │ +│ Sage Version 6.6.beta6, Release Date: 2015-03-19 │ │ Type "notebook()" for the browser-based notebook interface. │ │ Type "help()" for help. │ └────────────────────────────────────────────────────────────────────┘ diff --git a/src/bin/sage-version.sh b/src/bin/sage-version.sh index c30b38b1b7a..031407ec8e9 100644 --- a/src/bin/sage-version.sh +++ b/src/bin/sage-version.sh @@ -1,4 +1,4 @@ # Sage version information for shell scripts # This file is auto-generated by the sage-update-version script, do not edit! -SAGE_VERSION='6.6.beta5' -SAGE_RELEASE_DATE='2015-03-13' +SAGE_VERSION='6.6.beta6' +SAGE_RELEASE_DATE='2015-03-19' diff --git a/src/c_lib/SConstruct b/src/c_lib/SConstruct index 2420f39ae3d..5bd914ab3d1 100644 --- a/src/c_lib/SConstruct +++ b/src/c_lib/SConstruct @@ -128,13 +128,11 @@ env['PYV']=platform.python_version().rsplit('.', 1)[0] # whitespace without the syntax clutter of lists of strings. includes = ['$SAGE_LOCAL/include/', '$SAGE_LOCAL/include/python$PYV/', '$SAGE_LOCAL/include/NTL/', 'include'] -cFiles = Split( "interrupt.c mpn_pylong.c mpz_pylong.c") + \ - Split( "mpz_longlong.c stdsage.c" ) +cFiles = Split( "interrupt.c") cppFiles = Split( "ntl_wrap.cpp" ) srcFiles = cFiles + cppFiles -incFiles = Split( "ccobject.h" ) + \ - Split( "interrupt.h mpn_pylong.h mpz_longlong.h" ) + \ - Split( "mpz_pylong.h ntl_wrap.h parisage.h stdsage.h" ) +incFiles = Split( "interrupt.h" ) + \ + Split( "ntl_wrap.h" ) lib = env.SharedLibrary( "csage", [ "src/" + x for x in srcFiles ], LIBS=['ntl', 'pari', 'gmp', 'python$PYV'], diff --git a/src/c_lib/include/interrupt.h b/src/c_lib/include/interrupt.h index d04cbe1e935..edacc0aa97a 100644 --- a/src/c_lib/include/interrupt.h +++ b/src/c_lib/include/interrupt.h @@ -18,8 +18,8 @@ redirects stdin from /dev/null, to cause interactive sessions to exit. These are critical because they cannot be ignored. If they happen outside of sig_on(), we can only exit Sage with the dreaded "unhandled SIG..." message. Inside of sig_on(), they can be handled -and raise various exceptions (see sage/ext/c_lib.pyx). SIGQUIT will -never be handled and always causes Sage to exit. +and raise various exceptions (see sage/ext/interrupt.pyx). SIGQUIT +will never be handled and always causes Sage to exit. AUTHORS: @@ -108,9 +108,6 @@ void sage_signal_handler(int sig); /* * Setup the signal handlers. It is safe to call this more than once. - * - * We do not handle SIGALRM since there is code to deal with - * alarms in sage/misc/misc.py */ void setup_sage_signal_handler(void); diff --git a/src/c_lib/include/mpn_pylong.h b/src/c_lib/include/mpn_pylong.h deleted file mode 100644 index be5b929d705..00000000000 --- a/src/c_lib/include/mpn_pylong.h +++ /dev/null @@ -1,49 +0,0 @@ -#ifndef MPN_PYLONG_H -#define MPN_PYLONG_H - -#include -#include - -/************************************************************/ - -/* Python internals for pylong */ - -#include -typedef int py_size_t; /* what python uses for ob_size */ - -/************************************************************/ - -/* mpn -> pylong conversion */ - -int mpn_pylong_size (mp_ptr up, mp_size_t un); - -/* Assume digits points to a chunk of size size - * where size >= mpn_pylong_size(up, un) - */ -void mpn_get_pylong (digit *digits, py_size_t size, mp_ptr up, mp_size_t un); - -/************************************************************/ - -/* pylong -> mpn conversion */ - -mp_size_t mpn_size_from_pylong (digit *digits, py_size_t size); - -/* Assume up points to a chunk of size un - * where un >= mpn_size_from_pylong(digits, size) - */ -void mpn_set_pylong (mp_ptr up, mp_size_t un, digit *digits, py_size_t size); - -/************************************************************/ - -/* Python hashing */ - -/* - * for an mpz, this number has to be multiplied by the sign - * also remember to catch -1 and map it to -2 ! - */ - -long mpn_pythonhash (mp_ptr up, mp_size_t un); - -/************************************************************/ - -#endif diff --git a/src/c_lib/include/mpz_longlong.h b/src/c_lib/include/mpz_longlong.h deleted file mode 100644 index ebee1a5c11c..00000000000 --- a/src/c_lib/include/mpz_longlong.h +++ /dev/null @@ -1,6 +0,0 @@ - -#include - -void mpz_set_longlong(mpz_ptr z, long long val); - -void mpz_set_ulonglong(mpz_ptr z, unsigned long long val); diff --git a/src/c_lib/include/mpz_pylong.h b/src/c_lib/include/mpz_pylong.h deleted file mode 100644 index 8523ccc2e15..00000000000 --- a/src/c_lib/include/mpz_pylong.h +++ /dev/null @@ -1,26 +0,0 @@ -#ifndef MPZ_PYLONG_H -#define MPZ_PYLONG_H - -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/* mpz -> pylong conversion */ -PyObject * mpz_get_pylong(mpz_srcptr z); - -/* mpz -> pyint/pylong conversion */ -PyObject * mpz_get_pyintlong(mpz_srcptr z); - -/* pylong -> mpz conversion */ -int mpz_set_pylong(mpz_ptr z, PyObject * ll); - -/* mpz python hash */ -long mpz_pythonhash (mpz_srcptr z); - -#ifdef __cplusplus -} /* extern "C" */ -#endif -#endif diff --git a/src/c_lib/include/ntl_wrap.h b/src/c_lib/include/ntl_wrap.h index d1d6395420a..0d6c3e01f43 100644 --- a/src/c_lib/include/ntl_wrap.h +++ b/src/c_lib/include/ntl_wrap.h @@ -31,7 +31,6 @@ using namespace NTL; #endif #include "Python.h" -#include "ccobject.h" EXTERN void del_charstar(char*); diff --git a/src/c_lib/include/stdsage.h b/src/c_lib/include/stdsage.h deleted file mode 100644 index 4d40b55f192..00000000000 --- a/src/c_lib/include/stdsage.h +++ /dev/null @@ -1,154 +0,0 @@ -/****************************************************************************** - Copyright (C) 2006 William Stein - 2006 Martin Albrecht - - Distributed under the terms of the GNU General Public License (GPL) - as published by the Free Software Foundation; either version 2 of - the License, or (at your option) any later version. - http://www.gnu.org/licenses/ - -******************************************************************************/ - -/** - * @file stdsage.h - * - * @author William Stein - * @auhtor Martin Albrecht - * - * @brief General C (.h) code this is useful to include in any pyrex module. - * - * Put - @verbatim - - include 'relative/path/to/stdsage.pxi' - - @endverbatim - * - * at the top of your Pyrex file. - * - * These are mostly things that can't be done in Pyrex. - */ - -#ifndef STDSAGE_H -#define STDSAGE_H - -#include "Python.h" - -/* Building with this not commented out causes - serious problems on RHEL5 64-bit for Kiran Kedlaya... i.e., it doesn't work. */ -/* #include "ccobject.h" */ - -#ifdef __cplusplus -extern "C" { -#endif - - -/****************************************** - Some macros exported for Pyrex in cdefs.pxi - ****************************************/ - -/** Tests whether zzz_obj is of type zzz_type. The zzz_type must be a - * built-in or extension type. This is just a C++-compatible wrapper - * for PyObject_TypeCheck. - */ -#define PY_TYPE_CHECK(zzz_obj, zzz_type) \ - (PyObject_TypeCheck((PyObject*)(zzz_obj), (PyTypeObject*)(zzz_type))) - -/** Tests whether zzz_obj is exactly of type zzz_type. The zzz_type must be a - * built-in or extension type. - */ -#define PY_TYPE_CHECK_EXACT(zzz_obj, zzz_type) \ - ((PyTypeObject*)PY_TYPE(zzz_obj) == (PyTypeObject*)(zzz_type)) - -/** Returns the type field of a python object, cast to void*. The - * returned value should only be used as an opaque object e.g. for - * type comparisons. - */ -#define PY_TYPE(zzz_obj) ((void*)((zzz_obj)->ob_type)) - -/** Constructs a new object of type zzz_type by calling tp_new - * directly, with no arguments. - */ - -#define PY_NEW(zzz_type) \ - (((PyTypeObject*)(zzz_type))->tp_new((PyTypeObject*)(zzz_type), global_empty_tuple, NULL)) - - - /** Constructs a new object of type the same type as zzz_obj by calling tp_new - * directly, with no arguments. - */ - -#define PY_NEW_SAME_TYPE(zzz_obj) \ - PY_NEW(PY_TYPE(zzz_obj)) - -/** Resets the tp_new slot of zzz_type1 to point to the tp_new slot of - * zzz_type2. This is used in SAGE to speed up Pyrex's boilerplate - * object construction code by skipping irrelevant base class tp_new - * methods. - */ -#define PY_SET_TP_NEW(zzz_type1, zzz_type2) \ - (((PyTypeObject*)zzz_type1)->tp_new = ((PyTypeObject*)zzz_type2)->tp_new) - - -/** - * Tests whether the given object has a python dictionary. - */ -#define HAS_DICTIONARY(zzz_obj) \ - (((PyObject*)(zzz_obj))->ob_type->tp_dictoffset != NULL) - -/** - * Very very unsafe access to the list of pointers to PyObject*'s - * underlying a list / sequence. This does error checking of any kind - * -- make damn sure you hand it a list or sequence! - */ -#define FAST_SEQ_UNSAFE(zzz_obj) \ - PySequence_Fast_ITEMS(PySequence_Fast(zzz_obj, "expected sequence type")) - -/** Returns the type field of a python object, cast to void*. The - * returned value should only be used as an opaque object e.g. for - * type comparisons. - */ -#define PY_IS_NUMERIC(zzz_obj) \ - (PyInt_Check(zzz_obj) || PyBool_Check(zzz_obj) || PyLong_Check(zzz_obj) || \ - PyFloat_Check(zzz_obj) || PyComplex_Check(zzz_obj)) - - -/** This is exactly the same as isinstance (and does return a Python - * bool), but the second argument must be a C-extension type -- so it - * can't be a Python class or a list. If you just want an int return - * value, i.e., aren't going to pass this back to Python, just use - * PY_TYPE_CHECK. - */ -#define IS_INSTANCE(zzz_obj, zzz_type) \ - PyBool_FromLong(PY_TYPE_CHECK(zzz_obj, zzz_type)) - - -/** - * A global empty python tuple object. This is used to speed up some - * python API calls where we want to avoid constructing a tuple every - * time. - */ - -extern PyObject* global_empty_tuple; - - -/** - * Initialisation of signal handlers, global variables, etc. Called - * exactly once at Sage start-up. - */ -void init_csage(void); - - - -/** - * a handy macro to be placed at the top of a function definition - * below the variable declarations to ensure a function is called once - * at maximum. - */ -#define _CALLED_ONLY_ONCE static int ncalls = 0; if (ncalls>0) return; else ncalls++ - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/src/c_lib/src/interrupt.c b/src/c_lib/src/interrupt.c index 459b2a02942..ab84beabe95 100644 --- a/src/c_lib/src/interrupt.c +++ b/src/c_lib/src/interrupt.c @@ -40,7 +40,6 @@ Interrupt and signal handling for Sage #ifdef __linux__ #include #endif -#include "stdsage.h" #include "interrupt.h" @@ -132,8 +131,8 @@ void sage_interrupt_handler(int sig) else { /* Set the Python interrupt indicator, which will cause the - * Python-level interrupt handler in sage/ext/c_lib.pyx to be - * called. */ + * Python-level interrupt handler in sage/ext/interrupt.pyx to + * be called. */ PyErr_SetInterrupt(); } diff --git a/src/c_lib/src/mpn_pylong.c b/src/c_lib/src/mpn_pylong.c deleted file mode 100644 index 91e0064fcca..00000000000 --- a/src/c_lib/src/mpn_pylong.c +++ /dev/null @@ -1,209 +0,0 @@ -/* mpn <-> pylong conversion and "pythonhash" for mpn - * - * Author: Gonzalo Tornaría - * Date: March 2006 - * License: GPL v2 or later - * - * the code to change the base to 2^PyLong_SHIFT is based on the function - * mpn_get_str from GNU MP, but the new bugs are mine - * - * this is free software: if it breaks, you get to keep all the pieces - */ - -#include "mpn_pylong.h" - -/* This code assumes that PyLong_SHIFT < GMP_NUMB_BITS */ -#if PyLong_SHIFT >= GMP_NUMB_BITS -#error "Python limb larger than GMP limb !!!" -#endif - -/* Use these "portable" (I hope) sizebits functions - * We could implement this in terms of count_leading_zeros from GMP, - * but it is not exported ! - */ -static const -unsigned char -__sizebits_tab[128] = -{ - 0,1,2,2,3,3,3,3,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, - 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, - 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, - 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7 -}; - -#if GMP_LIMB_BITS > 64 -#error "word size > 64 unsupported" -#endif - -static inline -unsigned long -mpn_sizebits(mp_ptr up, mp_size_t un) { - unsigned long cnt; - mp_limb_t x; - if (un==0) return 0; - cnt = (un - 1) * GMP_NUMB_BITS; - x = up[un - 1]; -#if GMP_LIMB_BITS > 32 - if ((x >> 32) != 0) { x >>= 32; cnt += 32; } -#endif -#if GMP_LIMB_BITS > 16 - if ((x >> 16) != 0) { x >>= 16; cnt += 16; } -#endif -#if GMP_LIMB_BITS > 8 - if ((x >> 8) != 0) { x >>= 8; cnt += 8; } -#endif - return cnt + ((x & 0x80) ? 8 : __sizebits_tab[x]); -} - -static inline -unsigned long -pylong_sizebits(digit *digits, py_size_t size) { - unsigned long cnt; - digit x; - if (size==0) return 0; - cnt = (size - 1) * PyLong_SHIFT; - x = digits[size - 1]; -#if PyLong_SHIFT > 32 - if ((x >> 32) != 0) { x >>= 32; cnt += 32; } -#endif -#if PyLong_SHIFT > 16 - if ((x >> 16) != 0) { x >>= 16; cnt += 16; } -#endif -#if PyLong_SHIFT > 8 - if ((x >> 8) != 0) { x >>= 8; cnt += 8; } -#endif - return cnt + ((x & 0x80) ? 8 : __sizebits_tab[x]); -} - - -/* mpn -> pylong conversion */ - -int -mpn_pylong_size (mp_ptr up, mp_size_t un) -{ - return (mpn_sizebits(up, un) + PyLong_SHIFT - 1) / PyLong_SHIFT; -} - -/* this is based from GMP code in mpn/get_str.c */ - -/* Assume digits points to a chunk of size size - * where size >= mpn_pylong_size(up, un) - */ -void -mpn_get_pylong (digit *digits, py_size_t size, mp_ptr up, mp_size_t un) -{ - mp_limb_t n1, n0; - mp_size_t i; - int bit_pos; - /* point past the allocated chunk */ - digit * s = digits + size; - - /* input length 0 is special ! */ - if (un == 0) { - while (size) digits[--size]=0; - return; - } - - i = un - 1; - n1 = up[i]; - bit_pos = size * PyLong_SHIFT - i * GMP_NUMB_BITS; - - for (;;) - { - bit_pos -= PyLong_SHIFT; - while (bit_pos >= 0) - { - *--s = (n1 >> bit_pos) & PyLong_MASK; - bit_pos -= PyLong_SHIFT; - } - if (i == 0) - break; - n0 = (n1 << -bit_pos) & PyLong_MASK; - n1 = up[--i]; - bit_pos += GMP_NUMB_BITS; - *--s = n0 | (n1 >> bit_pos); - } -} - -/* pylong -> mpn conversion */ - -mp_size_t -mpn_size_from_pylong (digit *digits, py_size_t size) -{ - return (pylong_sizebits(digits, size) + GMP_NUMB_BITS - 1) / GMP_NUMB_BITS; -} - -void -mpn_set_pylong (mp_ptr up, mp_size_t un, digit *digits, py_size_t size) -{ - mp_limb_t n1, d; - mp_size_t i; - int bit_pos; - /* point past the allocated chunk */ - digit * s = digits + size; - - /* input length 0 is special ! */ - if (size == 0) { - while (un) up[--un]=0; - return; - } - - i = un - 1; - n1 = 0; - bit_pos = size * PyLong_SHIFT - i * GMP_NUMB_BITS; - - for (;;) - { - bit_pos -= PyLong_SHIFT; - while (bit_pos >= 0) - { - d = (mp_limb_t) *--s; - n1 |= (d << bit_pos) & GMP_NUMB_MASK; - bit_pos -= PyLong_SHIFT; - } - if (i == 0) - break; - d = (mp_limb_t) *--s; - /* add some high bits of d; maybe none if bit_pos=-PyLong_SHIFT */ - up[i--] = n1 | (d & PyLong_MASK) >> -bit_pos; - bit_pos += GMP_NUMB_BITS; - n1 = (d << bit_pos) & GMP_NUMB_MASK; - } - up[0] = n1; -} - - -/************************************************************/ - -/* Hashing functions */ - -/* This is a bad hash... - * If we decide to give up pylong compatibility, we should research to - * find a decent (but fast) hash - * - * Some pointers to start: - * - * - * - */ -/* - * for an mpz, this number has to be multiplied by the sign - * also remember to catch -1 and map it to -2 ! - */ -long -mpn_pythonhash (mp_ptr up, mp_size_t un) -{ - /* Simply add all limbs */ - mp_limb_t h = 0; - mp_limb_t h0; - mp_size_t i; - for (i = 0; i < un; i++) - { - h0 = h; - h += up[i]; - /* Add 1 on overflow */ - if (h < h0) h++; - } - return h; -} - diff --git a/src/c_lib/src/mpz_longlong.c b/src/c_lib/src/mpz_longlong.c deleted file mode 100644 index 0932bf6f9ac..00000000000 --- a/src/c_lib/src/mpz_longlong.c +++ /dev/null @@ -1,64 +0,0 @@ -/* long long -> mpz conversion -* -* Author: Robert Bradshaw -* Date: November 2008 -* License: GPL v2 or later -* -* this is free software: if it breaks, you get to keep all the pieces -*/ - -#include "mpz_longlong.h" -#include - -void mpz_set_longlong(mpz_ptr z, long long val) { - if (sizeof(mp_limb_t) == sizeof(long long)) { - mpz_set_si(z, val); - } - else if (2*sizeof(mp_limb_t) == sizeof(long long)) { - mp_limb_t* limbs = (mp_limb_t*)&val; - int sign = (val > 0) - (val < 0); - val *= sign; - _mpz_realloc (z, 2); - const int endianness = 1; - if (((char*)&endianness)[0]) { - // little endian - z->_mp_d[0] = limbs[0]; - z->_mp_d[1] = limbs[1]; - } - else { - // big endian - z->_mp_d[0] = limbs[1]; - z->_mp_d[1] = limbs[0]; - } - z->_mp_size = sign * (2 - !z->_mp_d[1]); - } - else { - assert(sizeof(mp_limb_t) == sizeof(long long) || 2*sizeof(mp_limb_t) == sizeof(long long)); - } -} - - -void mpz_set_ulonglong(mpz_ptr z, unsigned long long val) { - if (sizeof(mp_limb_t) == sizeof(unsigned long long)) { - mpz_set_ui(z, (mp_limb_t)val); - } - else if (2*sizeof(mp_limb_t) == sizeof(unsigned long long)) { - mp_limb_t* limbs = (mp_limb_t*)&val; - _mpz_realloc (z, 2); - const int endianness = 1; - if (((char*)&endianness)[0]) { - // little endian - z->_mp_d[0] = limbs[0]; - z->_mp_d[1] = limbs[1]; - } - else { - // big endian - z->_mp_d[0] = limbs[1]; - z->_mp_d[1] = limbs[0]; - } - z->_mp_size = (!!z->_mp_d[1] + (z->_mp_d[1] || z->_mp_d[0])); // 0, 1, or 2 - } - else { - assert(sizeof(mp_limb_t) == sizeof(unsigned long long) || 2*sizeof(mp_limb_t) == sizeof(unsigned long long)); - } -} diff --git a/src/c_lib/src/mpz_pylong.c b/src/c_lib/src/mpz_pylong.c deleted file mode 100644 index 843b187d648..00000000000 --- a/src/c_lib/src/mpz_pylong.c +++ /dev/null @@ -1,81 +0,0 @@ -/* mpz <-> pylong conversion and "pythonhash" for mpz - * - * Author: Gonzalo Tornaría - * Date: March 2006 - * License: GPL v2 or later - * - * this is free software: if it breaks, you get to keep all the pieces - -AUTHORS: - -- David Harvey (2007-08-18): added mpz_get_pyintlong function - - */ - -#include "mpn_pylong.h" -#include "mpz_pylong.h" - -/* mpz python hash */ -long -mpz_pythonhash (mpz_srcptr z) -{ - long x = mpn_pythonhash(z->_mp_d, abs(z->_mp_size)); - if (z->_mp_size < 0) - x = -x; - if (x == -1) - x = -2; - return x; -} - -/* mpz -> pylong conversion */ -PyObject * -mpz_get_pylong(mpz_srcptr z) -{ - py_size_t size = mpn_pylong_size(z->_mp_d, abs(z->_mp_size)); - PyLongObject *l = PyObject_NEW_VAR(PyLongObject, &PyLong_Type, size); - - if (l != NULL) - { - mpn_get_pylong(l->ob_digit, size, z->_mp_d, abs(z->_mp_size)); - if (z->_mp_size < 0) - Py_SIZE(l) = -Py_SIZE(l); - } - - return (PyObject *) l; -} - -/* mpz -> pyint/pylong conversion; if the value fits in a python int, it -returns a python int (optimised for that pathway), otherwise returns -a python long */ -PyObject * -mpz_get_pyintlong(mpz_srcptr z) -{ - if (mpz_fits_slong_p(z)) - return PyInt_FromLong(mpz_get_si(z)); - - return mpz_get_pylong(z); -} - -/* pylong -> mpz conversion */ -int -mpz_set_pylong(mpz_ptr z, PyObject * ll) -{ - register PyLongObject * l = (PyLongObject *) ll; - mp_size_t size; - int i; - - if (l==NULL || !PyLong_Check(l)) { - PyErr_BadInternalCall(); - return -1; - } - - size = mpn_size_from_pylong(l->ob_digit, abs(Py_SIZE(l))); - - if (z->_mp_alloc < size) - _mpz_realloc (z, size); - - mpn_set_pylong(z->_mp_d, size, l->ob_digit, abs(Py_SIZE(l))); - z->_mp_size = Py_SIZE(l) < 0 ? -size : size; - - return size; -} - diff --git a/src/c_lib/src/stdsage.c b/src/c_lib/src/stdsage.c deleted file mode 100644 index 7a4b88ed7c9..00000000000 --- a/src/c_lib/src/stdsage.c +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @file stdsage.c - * - * Some global C stuff that gets imported into pyrex modules. - * - */ - -/****************************************************************************** - * Copyright (C) 2006 William Stein - * 2006 David Harvey - * 2006 Martin Albrecht - * 2011 Jeroen Demeyer - * - * Distributed under the terms of the GNU General Public License (GPL) - * as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * http://www.gnu.org/licenses/ - ****************************************************************************/ - - -#include "stdsage.h" -#include "interrupt.h" - -PyObject* global_empty_tuple; - -void init_global_empty_tuple(void) { - _CALLED_ONLY_ONCE; - - global_empty_tuple = PyTuple_New(0); -} - - -/* This is called once during Sage startup. On some platforms like - * Cygwin, this is also called from init_csage_module(). */ -void init_csage() { - init_global_empty_tuple(); - setup_sage_signal_handler(); -} diff --git a/src/doc/common/builder.py b/src/doc/common/builder.py index 72e10bf19ca..0033697f455 100644 --- a/src/doc/common/builder.py +++ b/src/doc/common/builder.py @@ -633,7 +633,10 @@ def get_all_documents(self, refdir): sage: b = builder.ReferenceBuilder('reference') sage: refdir = os.path.join(os.environ['SAGE_DOC'], 'en', b.name) sage: sorted(b.get_all_documents(refdir)) - ['reference/algebras', 'reference/arithgroup', ..., 'reference/tensor'] + ['reference/algebras', + 'reference/arithgroup', + ..., + 'reference/tensor_free_modules'] """ documents = [] diff --git a/src/doc/en/constructions/linear_codes.rst b/src/doc/en/constructions/linear_codes.rst index e40913b91a3..621a9576eab 100644 --- a/src/doc/en/constructions/linear_codes.rst +++ b/src/doc/en/constructions/linear_codes.rst @@ -27,7 +27,7 @@ Sage can compute Hamming codes Linear code of length 13, dimension 10 over Finite Field of size 3 sage: C.minimum_distance() 3 - sage: C.gen_mat() + sage: C.generator_matrix() [1 0 0 0 0 0 0 0 0 0 1 2 0] [0 1 0 0 0 0 0 0 0 0 0 1 2] [0 0 1 0 0 0 0 0 0 0 1 0 2] @@ -51,7 +51,7 @@ the four Golay codes Linear code of length 12, dimension 6 over Finite Field of size 3 sage: C.minimum_distance() 6 - sage: C.gen_mat() + sage: C.generator_matrix() [1 0 0 0 0 0 2 0 1 2 1 2] [0 1 0 0 0 0 1 2 2 2 1 0] [0 0 1 0 0 0 1 1 1 0 1 1] @@ -79,12 +79,12 @@ a check matrix, and the dual code: sage: C; Cperp Linear code of length 7, dimension 4 over Finite Field of size 2 Linear code of length 7, dimension 3 over Finite Field of size 2 - sage: C.gen_mat() + sage: C.generator_matrix() [1 0 0 0 0 1 1] [0 1 0 0 1 0 1] [0 0 1 0 1 1 0] [0 0 0 1 1 1 1] - sage: C.check_mat() + sage: C.parity_check_matrix() [1 0 1 0 1 0 1] [0 1 1 0 0 1 1] [0 0 0 1 1 1 1] diff --git a/src/doc/en/reference/geometry/index.rst b/src/doc/en/reference/geometry/index.rst index dc118ccee30..4e80e6b7e4f 100644 --- a/src/doc/en/reference/geometry/index.rst +++ b/src/doc/en/reference/geometry/index.rst @@ -1,5 +1,8 @@ +Geometry +======== + Combinatorial Geometry -====================== +---------------------- Sage includes classes for convex rational polyhedral cones and fans, Groebner fans, lattice and reflexive polytopes (with integral coordinates), and generic @@ -38,6 +41,17 @@ polytopes and polyhedra (with rational or numerical coordinates). sage/geometry/linear_expression +Hyperbolic Geometry +------------------- + +.. toctree:: + :maxdepth: 1 + + sage/geometry/hyperbolic_space/hyperbolic_point + sage/geometry/hyperbolic_space/hyperbolic_isometry + sage/geometry/hyperbolic_space/hyperbolic_geodesic + sage/geometry/hyperbolic_space/hyperbolic_model + sage/geometry/hyperbolic_space/hyperbolic_interface Backends for Polyhedral Computations ------------------------------------ diff --git a/src/doc/en/reference/graphs/index.rst b/src/doc/en/reference/graphs/index.rst index 055b93bdfca..65ef36fce4c 100644 --- a/src/doc/en/reference/graphs/index.rst +++ b/src/doc/en/reference/graphs/index.rst @@ -78,7 +78,6 @@ Libraries of algorithms sage/graphs/graph_decompositions/rankwidth sage/graphs/graph_decompositions/bandwidth sage/graphs/graph_decompositions/graph_products - sage/graphs/modular_decomposition/modular_decomposition sage/graphs/convexity_properties sage/graphs/weakly_chordal sage/graphs/distances_all_pairs diff --git a/src/doc/en/reference/index.rst b/src/doc/en/reference/index.rst index 6526347a92c..72e9b3bd8b7 100644 --- a/src/doc/en/reference/index.rst +++ b/src/doc/en/reference/index.rst @@ -59,6 +59,7 @@ Linear Algebra * :doc:`Matrices and Spaces of Matrices ` * :doc:`Vectors and Modules ` +* :doc:`Tensors on free modules of finite rank ` Discrete Mathematics -------------------- diff --git a/src/doc/en/reference/libs/index.rst b/src/doc/en/reference/libs/index.rst index 90a489c13a6..b0faf6b0203 100644 --- a/src/doc/en/reference/libs/index.rst +++ b/src/doc/en/reference/libs/index.rst @@ -67,6 +67,9 @@ to be aware of the modules described in this chapter. sage/gsl/gsl_array + sage/ext/interrupt + sage/ext/pselect + .. Cannot be imported independently of mpmath: sage/libs/mpmath/ext_main sage/libs/mpmath/ext_impl sage/libs/mpmath/ext_libmp .. Modules depending on optional packages: sage/libs/coxeter3/coxeter sage/libs/coxeter3/coxeter_group sage/libs/fes diff --git a/src/doc/en/reference/modules/index.rst b/src/doc/en/reference/modules/index.rst index 7ed5db1ef0b..82bb1a6a3cc 100644 --- a/src/doc/en/reference/modules/index.rst +++ b/src/doc/en/reference/modules/index.rst @@ -8,6 +8,7 @@ Modules sage/modules/free_module sage/modules/free_module_integer sage/modules/free_module_element + sage/tensor/modules/finite_rank_free_module sage/modules/complex_double_vector sage/modules/real_double_vector diff --git a/src/doc/en/reference/tensor_free_modules/alt_forms.rst b/src/doc/en/reference/tensor_free_modules/alt_forms.rst new file mode 100644 index 00000000000..bbb0a9d966b --- /dev/null +++ b/src/doc/en/reference/tensor_free_modules/alt_forms.rst @@ -0,0 +1,9 @@ +Alternating forms +================= + +.. toctree:: + :maxdepth: 2 + + sage/tensor/modules/ext_pow_free_module + + sage/tensor/modules/free_module_alt_form diff --git a/src/doc/en/reference/tensor_free_modules/conf.py b/src/doc/en/reference/tensor_free_modules/conf.py new file mode 120000 index 00000000000..2bdf7e68470 --- /dev/null +++ b/src/doc/en/reference/tensor_free_modules/conf.py @@ -0,0 +1 @@ +../conf_sub.py \ No newline at end of file diff --git a/src/doc/en/reference/tensor_free_modules/index.rst b/src/doc/en/reference/tensor_free_modules/index.rst new file mode 100644 index 00000000000..c8f0b58b988 --- /dev/null +++ b/src/doc/en/reference/tensor_free_modules/index.rst @@ -0,0 +1,26 @@ +.. _tensors-on-free-modules: + +Tensors on free modules of finite rank +====================================== + +This work is part of the +`SageManifolds project `_ but it does +not depend upon other SageManifolds classes. In other words, it constitutes +a self-consistent subset that can be used independently of SageManifolds. + +.. toctree:: + :maxdepth: 2 + + sage/tensor/modules/finite_rank_free_module + + sage/tensor/modules/free_module_basis + + tensors + + alt_forms + + morphisms + + sage/tensor/modules/comp + +.. include:: ../footer.txt diff --git a/src/doc/en/reference/tensor_free_modules/morphisms.rst b/src/doc/en/reference/tensor_free_modules/morphisms.rst new file mode 100644 index 00000000000..fa661c8978d --- /dev/null +++ b/src/doc/en/reference/tensor_free_modules/morphisms.rst @@ -0,0 +1,13 @@ +Morphisms +========= + +.. toctree:: + :maxdepth: 2 + + sage/tensor/modules/free_module_homset + + sage/tensor/modules/free_module_morphism + + sage/tensor/modules/free_module_linear_group + + sage/tensor/modules/free_module_automorphism diff --git a/src/doc/en/reference/tensor_free_modules/tensors.rst b/src/doc/en/reference/tensor_free_modules/tensors.rst new file mode 100644 index 00000000000..be87fc68ad1 --- /dev/null +++ b/src/doc/en/reference/tensor_free_modules/tensors.rst @@ -0,0 +1,11 @@ +Tensors +======= + +.. toctree:: + :maxdepth: 2 + + sage/tensor/modules/tensor_free_module + + sage/tensor/modules/free_module_tensor + + sage/tensor/modules/tensor_with_indices diff --git a/src/doc/en/thematic_tutorials/coding_theory.rst b/src/doc/en/thematic_tutorials/coding_theory.rst index 378f8c3036c..5e5f7992183 100644 --- a/src/doc/en/thematic_tutorials/coding_theory.rst +++ b/src/doc/en/thematic_tutorials/coding_theory.rst @@ -142,7 +142,7 @@ call GUAVA: sage: G.order() # not tested (see trac #17617) 168 sage: C = codes.HammingCode(3,GF(2)) - sage: C.gen_mat() # not tested (see trac #17617) + sage: C.generator_matrix() # not tested (see trac #17617) [1 0 0 1 0 1 0] [0 1 0 1 0 1 1] [0 0 1 1 0 0 1] @@ -152,7 +152,7 @@ call GUAVA: [1 1 1] [1 0 1] [0 1 1] - sage: C.standard_form()[0].gen_mat() # not tested (see trac #17617) + sage: C.standard_form()[0].generator_matrix() # not tested (see trac #17617) [1 0 0 0 1 1 0] [0 1 0 0 1 1 1] [0 0 1 0 1 0 1] @@ -417,7 +417,7 @@ Python: sage: g = x^3+x+1 sage: C = codes.CyclicCodeFromGeneratingPolynomial(7,g); C Linear code of length 7, dimension 4 over Finite Field of size 2 - sage: C.gen_mat() + sage: C.generator_matrix() [1 1 0 1 0 0 0] [0 1 1 0 1 0 0] [0 0 1 1 0 1 0] @@ -425,7 +425,7 @@ Python: sage: g = x+1 sage: C = codes.CyclicCodeFromGeneratingPolynomial(4,g); C Linear code of length 4, dimension 3 over Finite Field of size 2 - sage: C.gen_mat() + sage: C.generator_matrix() [1 1 0 0] [0 1 1 0] [0 0 1 1] @@ -434,7 +434,7 @@ Python: Linear code of length 4, dimension 1 over Finite Field of size 3 sage: C = codes.CyclicCodeFromCheckPolynomial(4,x^3 + x^2 + x + 1); C Linear code of length 4, dimension 3 over Finite Field of size 3 - sage: C.gen_mat() + sage: C.generator_matrix() [2 1 0 0] [0 2 1 0] [0 0 2 1] @@ -515,20 +515,20 @@ Python: sage: C = codes.HammingCode(3,GF(2)) - sage: H = C.check_mat(); H # not tested (see trac #17617) + sage: H = C.parity_check_matrix(); H # not tested (see trac #17617) [1 0 0 1 1 0 1] [0 1 0 1 0 1 1] [0 0 1 1 1 1 0] sage: codes.LinearCodeFromCheckMatrix(H) == C # not tested (see trac #17617) True sage: C = codes.HammingCode(2,GF(3)) - sage: H = C.check_mat(); H # not tested (see trac #17617) + sage: H = C.parity_check_matrix(); H # not tested (see trac #17617) [1 0 2 2] [0 1 2 1] sage: codes.LinearCodeFromCheckMatrix(H) == C # not tested (see trac #17617) True sage: C = codes.RandomLinearCode(10,5,GF(4,"a")) - sage: H = C.check_mat() + sage: H = C.parity_check_matrix() sage: codes.LinearCodeFromCheckMatrix(H) == C # not tested (see trac #17617) True diff --git a/src/module_list.py b/src/module_list.py index 4deedc717a0..f7951296fcc 100755 --- a/src/module_list.py +++ b/src/module_list.py @@ -404,11 +404,6 @@ def uname_specific(name, value, alternative): Extension('sage.graphs.base.static_sparse_backend', sources = ['sage/graphs/base/static_sparse_backend.pyx']), - Extension('sage.graphs.modular_decomposition.modular_decomposition', - sources = ['sage/graphs/modular_decomposition/modular_decomposition.pyx', - 'sage/graphs/modular_decomposition/src/dm.c'], - depends = ['sage/graphs/modular_decomposition/src/dm_english.h']), - Extension('sage.graphs.weakly_chordal', sources = ['sage/graphs/weakly_chordal.pyx']), @@ -2081,7 +2076,12 @@ def uname_specific(name, value, alternative): Extension("sage.graphs.mcqd", ["sage/graphs/mcqd.pyx"], language = "c++")) -# libraries = ["mcqd"])) + +if is_package_installed('modular_decomposition'): + ext_modules.append( + Extension('sage.graphs.modular_decomposition', + sources = ['sage/graphs/modular_decomposition.pyx'], + libraries = ['modulardecomposition'])) if is_package_installed('arb'): ext_modules.extend([ diff --git a/src/sage/.gitignore b/src/sage/.gitignore new file mode 100644 index 00000000000..c1d376abd3c --- /dev/null +++ b/src/sage/.gitignore @@ -0,0 +1,2 @@ +/libs/pari/auto_gen.pxi +/libs/pari/auto_instance.pxi diff --git a/src/sage/all.py b/src/sage/all.py index ca886cb9e08..d2494e0e262 100644 --- a/src/sage/all.py +++ b/src/sage/all.py @@ -75,14 +75,11 @@ ################################################################### -import sage.ext.c_lib -sage.ext.c_lib._init_csage() -sig_on_count = sage.ext.c_lib._sig_on_reset +# This import also setups the interrupt handler +from sage.ext.interrupt import AlarmInterrupt, SignalError, sig_on_reset as sig_on_count from time import sleep -from sage.ext.c_lib import AlarmInterrupt, SignalError - import sage.misc.lazy_import from sage.misc.all import * # takes a while from sage.repl.all import * diff --git a/src/sage/categories/crystals.py b/src/sage/categories/crystals.py index 8786ac5cb5d..6d3d516251d 100644 --- a/src/sage/categories/crystals.py +++ b/src/sage/categories/crystals.py @@ -1190,6 +1190,52 @@ def to_lowest_weight(self, index_set = None): return [lw[0], [i] + lw[1]] return [self, []] + def all_paths_to_highest_weight(self, index_set=None): + r""" + Return all paths to the highest weight from ``self`` with respect + to `index_set`. + + INPUT: + + - ``index_set`` -- (optional) a subset of the index set of ``self`` + + EXAMPLES:: + + sage: B = crystals.infinity.Tableaux("A2") + sage: b0 = B.highest_weight_vector() + sage: b = b0.f_string([1, 2, 1, 2]) + sage: L = b.all_paths_to_highest_weight() + sage: list(L) + [[2, 1, 2, 1], [2, 2, 1, 1]] + + sage: Y = crystals.infinity.GeneralizedYoungWalls(3) + sage: y0 = Y.highest_weight_vector() + sage: y = y0.f_string([0, 1, 2, 3, 2, 1, 0]) + sage: list(y.all_paths_to_highest_weight()) + [[0, 1, 2, 3, 2, 1, 0], + [0, 1, 3, 2, 2, 1, 0], + [0, 3, 1, 2, 2, 1, 0], + [0, 3, 2, 1, 1, 0, 2], + [0, 3, 2, 1, 1, 2, 0]] + + sage: B = crystals.Tableaux("A3", shape=[4,2,1]) + sage: b0 = B.highest_weight_vector() + sage: b = b0.f_string([1, 1, 2, 3]) + sage: list(b.all_paths_to_highest_weight()) + [[1, 3, 2, 1], [3, 1, 2, 1], [3, 2, 1, 1]] + """ + if index_set is None: + index_set = self.index_set() + hw = True + for i in index_set: + next = self.e(i) + if next is not None: + for x in next.all_paths_to_highest_weight(index_set): + yield [i] + x + hw = False + if hw: + yield [] + def subcrystal(self, index_set=None, max_depth=float("inf"), direction="both"): r""" Construct the subcrystal generated by ``self`` using `e_i` and/or diff --git a/src/sage/coding/binary_code.pyx b/src/sage/coding/binary_code.pyx index d8a595f62e9..cc8a85ff07f 100644 --- a/src/sage/coding/binary_code.pyx +++ b/src/sage/coding/binary_code.pyx @@ -1097,7 +1097,7 @@ cdef class BinaryCode: EXAMPLE: sage: from sage.coding.binary_code import * - sage: B = BinaryCode(codes.ExtendedBinaryGolayCode().gen_mat()) + sage: B = BinaryCode(codes.ExtendedBinaryGolayCode().generator_matrix()) sage: B Binary [24,12] linear code, generator matrix [100000000000101011100011] @@ -3855,7 +3855,7 @@ cdef class BinaryCodeClassifier: EXAMPLE: sage: from sage.coding.binary_code import * sage: BC = BinaryCodeClassifier() - sage: B = BinaryCode(codes.ExtendedBinaryGolayCode().gen_mat()) + sage: B = BinaryCode(codes.ExtendedBinaryGolayCode().generator_matrix()) sage: B.apply_permutation(range(24,-1,-1)) sage: B Binary [24,12] linear code, generator matrix diff --git a/src/sage/coding/code_constructions.py b/src/sage/coding/code_constructions.py index e04591497fb..941013ef9e9 100644 --- a/src/sage/coding/code_constructions.py +++ b/src/sage/coding/code_constructions.py @@ -646,7 +646,7 @@ def CyclicCodeFromGeneratingPolynomial(n,g,ignore=True): sage: g = x^3+x+1 sage: C = codes.CyclicCodeFromGeneratingPolynomial(7,g); C Linear code of length 7, dimension 4 over Finite Field of size 2 - sage: C.gen_mat() + sage: C.generator_matrix() [1 1 0 1 0 0 0] [0 1 1 0 1 0 0] [0 0 1 1 0 1 0] @@ -654,7 +654,7 @@ def CyclicCodeFromGeneratingPolynomial(n,g,ignore=True): sage: g = x+1 sage: C = codes.CyclicCodeFromGeneratingPolynomial(4,g); C Linear code of length 4, dimension 3 over Finite Field of size 2 - sage: C.gen_mat() + sage: C.generator_matrix() [1 1 0 0] [0 1 1 0] [0 0 1 1] @@ -720,7 +720,7 @@ def CyclicCodeFromCheckPolynomial(n,h,ignore=True): Linear code of length 4, dimension 1 over Finite Field of size 3 sage: C = codes.CyclicCodeFromCheckPolynomial(4,x^3 + x^2 + x + 1); C Linear code of length 4, dimension 3 over Finite Field of size 3 - sage: C.gen_mat() + sage: C.generator_matrix() [2 1 0 0] [0 2 1 0] [0 0 2 1] @@ -994,7 +994,7 @@ def HammingCode(r,F): H = MS(PFn).transpose() Cd = LinearCode(H) # Hamming code always has distance 3, so we provide the distance. - return LinearCode(Cd.dual_code().gen_mat(), d=3) + return LinearCode(Cd.dual_code().generator_matrix(), d=3) def LinearCodeFromCheckMatrix(H): @@ -1019,20 +1019,20 @@ def LinearCodeFromCheckMatrix(H): EXAMPLES:: sage: C = codes.HammingCode(3,GF(2)) - sage: H = C.check_mat(); H + sage: H = C.parity_check_matrix(); H [1 0 1 0 1 0 1] [0 1 1 0 0 1 1] [0 0 0 1 1 1 1] sage: codes.LinearCodeFromCheckMatrix(H) == C True sage: C = codes.HammingCode(2,GF(3)) - sage: H = C.check_mat(); H + sage: H = C.parity_check_matrix(); H [1 0 1 1] [0 1 1 2] sage: codes.LinearCodeFromCheckMatrix(H) == C True sage: C = codes.RandomLinearCode(10,5,GF(4,"a")) - sage: H = C.check_mat() + sage: H = C.parity_check_matrix() sage: codes.LinearCodeFromCheckMatrix(H) == C True """ diff --git a/src/sage/coding/codecan/autgroup_can_label.pyx b/src/sage/coding/codecan/autgroup_can_label.pyx index cc8410d72c0..abe95aa08c9 100644 --- a/src/sage/coding/codecan/autgroup_can_label.pyx +++ b/src/sage/coding/codecan/autgroup_can_label.pyx @@ -49,14 +49,14 @@ EXAMPLES:: sage: from sage.coding.codecan.autgroup_can_label import LinearCodeAutGroupCanLabel sage: C = codes.HammingCode(3, GF(3)).dual_code() sage: P = LinearCodeAutGroupCanLabel(C) - sage: P.get_canonical_form().gen_mat() + sage: P.get_canonical_form().generator_matrix() [1 0 0 0 0 1 1 1 1 1 1 1 1] [0 1 0 1 1 0 0 1 1 2 2 1 2] [0 0 1 1 2 1 2 1 2 1 2 0 0] - sage: LinearCode(P.get_transporter()*C.gen_mat()) == P.get_canonical_form() + sage: LinearCode(P.get_transporter()*C.generator_matrix()) == P.get_canonical_form() True sage: A = P.get_autom_gens() - sage: all( [ LinearCode(a*C.gen_mat()) == C for a in A]) + sage: all( [ LinearCode(a*C.generator_matrix()) == C for a in A]) True sage: P.get_autom_order() == GL(3, GF(3)).order() True @@ -65,7 +65,7 @@ If the dimension of the dual code is smaller, we will work on this code:: sage: C2 = codes.HammingCode(3, GF(3)) sage: P2 = LinearCodeAutGroupCanLabel(C2) - sage: P2.get_canonical_form().check_mat() == P.get_canonical_form().gen_mat() + sage: P2.get_canonical_form().parity_check_matrix() == P.get_canonical_form().generator_matrix() True There is a specialization of this algorithm to pass a coloring on the @@ -171,14 +171,14 @@ class LinearCodeAutGroupCanLabel: sage: from sage.coding.codecan.autgroup_can_label import LinearCodeAutGroupCanLabel sage: C = codes.HammingCode(3, GF(3)).dual_code() sage: P = LinearCodeAutGroupCanLabel(C) - sage: P.get_canonical_form().gen_mat() + sage: P.get_canonical_form().generator_matrix() [1 0 0 0 0 1 1 1 1 1 1 1 1] [0 1 0 1 1 0 0 1 1 2 2 1 2] [0 0 1 1 2 1 2 1 2 1 2 0 0] - sage: LinearCode(P.get_transporter()*C.gen_mat()) == P.get_canonical_form() + sage: LinearCode(P.get_transporter()*C.generator_matrix()) == P.get_canonical_form() True sage: a = P.get_autom_gens()[0] - sage: (a*C.gen_mat()).echelon_form() == C.gen_mat().echelon_form() + sage: (a*C.generator_matrix()).echelon_form() == C.generator_matrix().echelon_form() True sage: P.get_autom_order() == GL(3, GF(3)).order() True @@ -208,13 +208,13 @@ class LinearCodeAutGroupCanLabel: sage: from sage.coding.codecan.autgroup_can_label import LinearCodeAutGroupCanLabel sage: C = codes.HammingCode(3, GF(2)).dual_code() sage: P = LinearCodeAutGroupCanLabel(C) - sage: P.get_canonical_form().gen_mat() + sage: P.get_canonical_form().generator_matrix() [1 0 0 0 1 1 1] [0 1 0 1 0 1 1] [0 0 1 1 1 1 0] sage: P2 = LinearCodeAutGroupCanLabel(C, P=[[0,3,5],[1,2,4,6]], ....: algorithm_type="permutational") - sage: P2.get_canonical_form().gen_mat() + sage: P2.get_canonical_form().generator_matrix() [1 1 1 0 0 0 1] [0 1 0 1 1 0 1] [0 0 1 0 1 1 1] @@ -226,7 +226,7 @@ class LinearCodeAutGroupCanLabel: raise TypeError("%s is not a linear code"%C) self.C = C - mat = C.gen_mat() + mat = C.generator_matrix() F = mat.base_ring() S = SemimonomialTransformationGroup(F, mat.ncols()) @@ -344,7 +344,7 @@ class LinearCodeAutGroupCanLabel: P=P_refined, algorithm_type=algorithm_type) can_transp = agcl.get_transporter() can_transp.invert_v() - can_col_set = agcl.get_canonical_form().check_mat().columns() + can_col_set = agcl.get_canonical_form().parity_check_matrix().columns() A = agcl.get_autom_gens() for a in A: a.invert_v() @@ -534,7 +534,7 @@ class LinearCodeAutGroupCanLabel: sage: C = codes.HammingCode(3, GF(3)).dual_code() sage: CF1 = LinearCodeAutGroupCanLabel(C).get_canonical_form() sage: s = SemimonomialTransformationGroup(GF(3), C.length()).an_element() - sage: C2 = LinearCode(s*C.gen_mat()) + sage: C2 = LinearCode(s*C.generator_matrix()) sage: CF2 = LinearCodeAutGroupCanLabel(C2).get_canonical_form() sage: CF1 == CF2 True @@ -552,7 +552,7 @@ class LinearCodeAutGroupCanLabel: sage: P = LinearCodeAutGroupCanLabel(C) sage: g = P.get_transporter() sage: D = P.get_canonical_form() - sage: (g*C.gen_mat()).echelon_form() == D.gen_mat().echelon_form() + sage: (g*C.generator_matrix()).echelon_form() == D.generator_matrix().echelon_form() True """ return self._transporter @@ -566,7 +566,7 @@ class LinearCodeAutGroupCanLabel: sage: from sage.coding.codecan.autgroup_can_label import LinearCodeAutGroupCanLabel sage: C = codes.HammingCode(3, GF(2)).dual_code() sage: A = LinearCodeAutGroupCanLabel(C).get_autom_gens() - sage: Gamma = C.gen_mat().echelon_form() + sage: Gamma = C.generator_matrix().echelon_form() sage: all([(g*Gamma).echelon_form() == Gamma for g in A]) True """ @@ -601,12 +601,12 @@ class LinearCodeAutGroupCanLabel: sage: from sage.coding.codecan.autgroup_can_label import LinearCodeAutGroupCanLabel sage: C = codes.HammingCode(3, GF(4, 'a')).dual_code() sage: A = LinearCodeAutGroupCanLabel(C).get_PGammaL_gens() - sage: Gamma = C.gen_mat() + sage: Gamma = C.generator_matrix() sage: N = [ x.monic() for x in Gamma.columns() ] sage: all([ (g[0]*n.apply_map(g[1])).monic() in N for n in N for g in A]) True """ - Gamma = self.C.gen_mat() + Gamma = self.C.generator_matrix() res = [] for a in self._PGammaL_autom_gens: B = Gamma.solve_left(a * Gamma, check=True) diff --git a/src/sage/coding/codecan/codecan.pyx b/src/sage/coding/codecan/codecan.pyx index d793c775cb8..802e5cf8996 100644 --- a/src/sage/coding/codecan/codecan.pyx +++ b/src/sage/coding/codecan/codecan.pyx @@ -62,7 +62,7 @@ EXAMPLES: Get the canonical form of the Simplex code:: sage: from sage.coding.codecan.codecan import PartitionRefinementLinearCode - sage: mat = codes.HammingCode(3, GF(3)).dual_code().gen_mat() + sage: mat = codes.HammingCode(3, GF(3)).dual_code().generator_matrix() sage: P = PartitionRefinementLinearCode(mat.ncols(), mat) sage: cf = P.get_canonical_form(); cf [1 0 0 0 0 1 1 1 1 1 1 1 1] @@ -470,7 +470,7 @@ cdef class PartitionRefinementLinearCode(PartitionRefinement_generic): EXAMPLES:: sage: from sage.coding.codecan.codecan import PartitionRefinementLinearCode - sage: mat = codes.HammingCode(3, GF(3)).dual_code().gen_mat() + sage: mat = codes.HammingCode(3, GF(3)).dual_code().generator_matrix() sage: P = PartitionRefinementLinearCode(mat.ncols(), mat) sage: cf = P.get_canonical_form(); cf [1 0 0 0 0 1 1 1 1 1 1 1 1] @@ -490,39 +490,39 @@ cdef class PartitionRefinementLinearCode(PartitionRefinement_generic): sage: all( [(a*mat).echelon_form() == mat.echelon_form() for a in A]) True """ - def __cinit__(self, n, gen_mat, **kwds): + def __cinit__(self, n, generator_matrix, **kwds): r""" Initialization. See :meth:`__init__`. EXAMPLES:: sage: from sage.coding.codecan.codecan import PartitionRefinementLinearCode - sage: mat = codes.HammingCode(3, GF(3)).dual_code().gen_mat() + sage: mat = codes.HammingCode(3, GF(3)).dual_code().generator_matrix() sage: P = PartitionRefinementLinearCode(mat.ncols(), mat) """ - self._k = gen_mat.nrows() - self._q = len(gen_mat.base_ring()) + self._k = generator_matrix.nrows() + self._q = len(generator_matrix.base_ring()) self._nr_of_supp_refine_calls = 0 self._nr_of_point_refine_calls = 0 - self._matrix = copy(gen_mat) - self._root_matrix = gen_mat + self._matrix = copy(generator_matrix) + self._root_matrix = generator_matrix self._stored_states = dict() self._supp_refine_vals = _BestValStore(n) self._point_refine_vals = _BestValStore(n) # self._hyp_refine_vals will initialized after # we computed the set of codewords - def __init__(self, n, gen_mat, P=None, algorithm_type="semilinear"): + def __init__(self, n, generator_matrix, P=None, algorithm_type="semilinear"): r""" Initialization, we immediately start the algorithm (see :mod:``sage.coding.codecan.codecan``) to compute the canonical form and automorphism group of the linear code - generated by ``gen_mat``. + generated by ``generator_matrix``. INPUT: - ``n`` -- an integer - - ``gen_mat`` -- a `k \times n` matrix over `\GF{q}` of full row rank, + - ``generator_matrix`` -- a `k \times n` matrix over `\GF{q}` of full row rank, i.e. `k = GF(4, 'a') - sage: mset = [x for x in K if x!=0] - sage: tuples(mset,2) - [[a, a], [a + 1, a], [1, a], [a, a + 1], [a + 1, a + 1], [1, a + 1], [a, 1], [a + 1, 1], [1, 1]] + sage: mset = [x for x in K if x != 0] + sage: tuples(mset, 2) + [(a, a), (a, a + 1), (a, 1), (a + 1, a), (a + 1, a + 1), + (a + 1, 1), (1, a), (1, a + 1), (1, 1)] + + We check that the implementations agree (up to ordering):: + + sage: tuples(S, 3, 'native') + [(1, 1, 1), (2, 1, 1), (1, 2, 1), (2, 2, 1), + (1, 1, 2), (2, 1, 2), (1, 2, 2), (2, 2, 2)] + + Lastly we check on a multiset:: + + sage: S = [1,1,2] + sage: sorted(tuples(S, 3)) == sorted(tuples(S, 3, 'native')) + True AUTHORS: - Jon Hanke (2006-08) """ - import copy - if k<=0: - return [[]] - if k==1: - return [[x] for x in S] + if algorithm == 'itertools': + import itertools + return list(itertools.product(S, repeat=k)) + if algorithm == 'native': + return _tuples_native(S, k) + raise ValueError('invalid algorithm') + +def _tuples_native(S, k): + """ + Return a list of all `k`-tuples of elements of a given set ``S``. + + This is a helper method used in :meth:`tuples`. It returns the + same as ``tuples(S, k, algorithm="native")``. + + EXAMPLES:: + + sage: S = [1,2,2] + sage: from sage.combinat.combinat import _tuples_native + sage: _tuples_native(S,2) + [(1, 1), (2, 1), (2, 1), (1, 2), (2, 2), (2, 2), + (1, 2), (2, 2), (2, 2)] + """ + if k <= 0: + return [()] + if k == 1: + return [(x,) for x in S] ans = [] for s in S: - for x in tuples(S,k-1): - y = copy.copy(x) + for x in _tuples_native(S, k-1): + y = list(x) y.append(s) - ans.append(y) + ans.append(tuple(y)) return ans -def number_of_tuples(S, k): +def number_of_tuples(S, k, algorithm='naive'): """ - Return the size of ``tuples(S,k)``. Wraps GAP's ``NrTuples``. + Return the size of ``tuples(S, k)`` when `S` is a set. More + generally, return the size of ``tuples(set(S), k)``. (So, + unlike :meth:`tuples`, this method removes redundant entries from + `S`.) + + INPUT: + + - ``S`` -- the base set + - ``k`` -- the length of the tuples + - ``algorithm`` -- can be one of the following: + + * ``'naive'`` - (default) use the naive counting `|S|^k` + * ``'gap'`` - wraps GAP's ``NrTuples`` + + .. WARNING:: + + When using ``algorithm='gap'``, ``S`` must be a list of objects + that have string representations that can be interpreted by the GAP + interpreter. If ``S`` consists of at all complicated Sage + objects, this function might *not* do what you expect. EXAMPLES:: sage: S = [1,2,3,4,5] sage: number_of_tuples(S,2) 25 + sage: number_of_tuples(S,2, algorithm="gap") + 25 sage: S = [1,1,2,3,4,5] sage: number_of_tuples(S,2) 25 + sage: number_of_tuples(S,2, algorithm="gap") + 25 + sage: number_of_tuples(S,0) + 1 + sage: number_of_tuples(S,0, algorithm="gap") + 1 """ - k = ZZ(k) - from sage.libs.gap.libgap import libgap - S = libgap.eval(str(S)) - return libgap.NrTuples(S, k).sage() + if algorithm == 'naive': + return ZZ( len(set(S)) )**k # The set is there to avoid duplicates + if algorithm == 'gap': + k = ZZ(k) + from sage.libs.gap.libgap import libgap + S = libgap.eval(str(S)) + return libgap.NrTuples(S, k).sage() + raise ValueError('invalid algorithm') -def unordered_tuples(S, k): - """ - Return the set of all unordered tuples of length ``k`` of the - set ``S``. Wraps GAP's ``UnorderedTuples``. +def unordered_tuples(S, k, algorithm='itertools'): + r""" + Return a list of all unordered tuples of length ``k`` of the set ``S``. - An unordered tuple of length `k` of a set `S` is a unordered selection - with repetitions of elements of `S`, and is represented by a sorted - list of length `k` containing elements from `S`. + An unordered tuple of length `k` of set `S` is a unordered selection + with repetitions of `S` and is represented by a sorted list of length + `k` containing elements from `S`. - .. WARNING:: + Unlike :meth:`tuples`, the result of this method does not depend on + how often an element appears in `S`; only the *set* `S` is being + used. For example, ``unordered_tuples([1, 1, 1], 2)`` will return + ``[(1, 1)]``. If you want it to return + ``[(1, 1), (1, 1), (1, 1)]``, use Python's + ``itertools.combinations_with_replacement`` instead. - Wraps GAP -- hence ``S`` must be a list of objects that have - string representations that can be interpreted by the GAP - interpreter. If ``S`` contains any complicated Sage - objects, this function does *not* do what you expect. A proper - function should be written! (TODO!) + INPUT: - .. NOTE:: + - ``S`` -- the base set + - ``k`` -- the length of the tuples + - ``algorithm`` -- can be one of the following: + + * ``'itertools'`` - (default) use python's itertools + * ``'gap'`` - wraps GAP's ``UnorderedTuples`` + + .. WARNING:: - Repeated entries in ``S`` are being ignored -- i.e., - ``unordered_tuples([1,2,3,3],2)`` doesn't return anything - different from ``unordered_tuples([1,2,3],2)``. + When using ``algorithm='gap'``, ``S`` must be a list of objects + that have string representations that can be interpreted by the GAP + interpreter. If ``S`` consists of at all complicated Sage + objects, this function might *not* do what you expect. EXAMPLES:: sage: S = [1,2] - sage: unordered_tuples(S,3) - [[1, 1, 1], [1, 1, 2], [1, 2, 2], [2, 2, 2]] - sage: unordered_tuples(["a","b","c"],2) - ['aa', 'ab', 'ac', 'bb', 'bc', 'cc'] - """ - k = ZZ(k) - from sage.libs.gap.libgap import libgap - S = libgap.eval(str(S)) - return libgap.UnorderedTuples(S, k).sage() + sage: unordered_tuples(S, 3) + [(1, 1, 1), (1, 1, 2), (1, 2, 2), (2, 2, 2)] + + We check that this agrees with GAP:: + + sage: unordered_tuples(S, 3, algorithm='gap') + [(1, 1, 1), (1, 1, 2), (1, 2, 2), (2, 2, 2)] + + We check the result on strings:: -def number_of_unordered_tuples(S,k): + sage: S = ["a","b","c"] + sage: unordered_tuples(S, 2) + [('a', 'a'), ('a', 'b'), ('a', 'c'), ('b', 'b'), ('b', 'c'), ('c', 'c')] + sage: unordered_tuples(S, 2, algorithm='gap') + [('a', 'a'), ('a', 'b'), ('a', 'c'), ('b', 'b'), ('b', 'c'), ('c', 'c')] + + Lastly we check on a multiset:: + + sage: S = [1,1,2] + sage: unordered_tuples(S, 3) == unordered_tuples(S, 3, 'gap') + True + sage: unordered_tuples(S, 3) + [(1, 1, 1), (1, 1, 2), (1, 2, 2), (2, 2, 2)] """ - Return the size of ``unordered_tuples(S,k)``. Wraps GAP's - ``NrUnorderedTuples``. + if algorithm == 'itertools': + import itertools + return list(itertools.combinations_with_replacement(sorted(set(S)), k)) + if algorithm == 'gap': + k = ZZ(k) + from sage.libs.gap.libgap import libgap + S = libgap.eval(str(S)) + return [tuple(x) for x in libgap.UnorderedTuples(S, k).sage()] + raise ValueError('invalid algorithm') + +def number_of_unordered_tuples(S, k, algorithm='naive'): + r""" + Return the size of ``unordered_tuples(S, k)`` when `S` is a set. + + INPUT: + + - ``S`` -- the base set + - ``k`` -- the length of the tuples + - ``algorithm`` -- can be one of the following: + + * ``'naive'`` - (default) use the naive counting `\binom{|S|+k-1}{k}` + * ``'gap'`` - wraps GAP's ``NrUnorderedTuples`` + + .. WARNING:: + + When using ``algorithm='gap'``, ``S`` must be a list of objects + that have string representations that can be interpreted by the GAP + interpreter. If ``S`` consists of at all complicated Sage + objects, this function might *not* do what you expect. EXAMPLES:: sage: S = [1,2,3,4,5] sage: number_of_unordered_tuples(S,2) 15 + sage: number_of_unordered_tuples(S,2, algorithm="gap") + 15 + sage: S = [1,1,2,3,4,5] + sage: number_of_unordered_tuples(S,2) + 15 + sage: number_of_unordered_tuples(S,2, algorithm="gap") + 15 + sage: number_of_unordered_tuples(S,0) + 1 + sage: number_of_unordered_tuples(S,0, algorithm="gap") + 1 """ - from sage.libs.gap.libgap import libgap - S = libgap.eval(str(S)) - return libgap.NrUnorderedTuples(S, k).sage() + if algorithm == 'naive': + return ZZ( len(set(S)) + k - 1 ).binomial(k) # The set is there to avoid duplicates + if algorithm == 'gap': + k = ZZ(k) + from sage.libs.gap.libgap import libgap + S = libgap.eval(str(S)) + return libgap.NrUnorderedTuples(S, k).sage() + raise ValueError('invalid algorithm') def unshuffle_iterator(a, one=1): r""" @@ -2523,7 +2661,7 @@ def bell_polynomial(n, k): OUTPUT: - - polynomial expression (SymbolicArithmetic) + - a polynomial in `n-k+1` variables over `\QQ` EXAMPLES:: diff --git a/src/sage/combinat/descent_algebra.py b/src/sage/combinat/descent_algebra.py index acb405ecf7b..b7ff208c65e 100644 --- a/src/sage/combinat/descent_algebra.py +++ b/src/sage/combinat/descent_algebra.py @@ -99,11 +99,16 @@ class DescentAlgebra(Parent, UniqueRepresentation): sage: I(elt) 7/6*I[1, 1, 1, 1] + 2*I[1, 1, 2] + 3*I[1, 2, 1] + 4*I[1, 3] - There is the following syntatic sugar for calling elements of a basis, note - that for the empty set one must use ``D[[]]`` due to python's syntax:: + + As syntactic sugar, one can use the notation ``D[i,...,l]`` to + construct elements of the basis; note that for the empty set one + must use ``D[[]]`` due to Python's syntax:: sage: D[[]] + D[2] + 2*D[1,2] D{} + 2*D{1, 2} + D{2} + + The same syntax works for the other bases:: + sage: I[1,2,1] + 3*I[4] + 2*I[3,1] I[1, 2, 1] + 2*I[3, 1] + 3*I[4] diff --git a/src/sage/combinat/matrices/dancing_links.pyx b/src/sage/combinat/matrices/dancing_links.pyx index df5936d0e2f..eaeafe2fb8c 100644 --- a/src/sage/combinat/matrices/dancing_links.pyx +++ b/src/sage/combinat/matrices/dancing_links.pyx @@ -41,9 +41,11 @@ cdef extern from "dancing_links_c.h": int search() void freemem() +cdef extern from "ccobject.h": dancing_links* dancing_links_construct "Construct"(void *mem) void dancing_links_destruct "Destruct"(dancing_links *mem) + from sage.rings.integer cimport Integer cdef class dancing_linksWrapper: diff --git a/src/sage/combinat/permutation.py b/src/sage/combinat/permutation.py index 389bf284fd0..90382a085e5 100644 --- a/src/sage/combinat/permutation.py +++ b/src/sage/combinat/permutation.py @@ -360,7 +360,7 @@ class Permutation(CombinatorialObject, Element): .. WARNING:: Since :trac:`13742` the input is checked for correctness : it is not - accepted unless actually is a permutation on `\{1, \ldots, n\}`. It + accepted unless it actually is a permutation on `\{1, \ldots, n\}`. It means that some :meth:`Permutation` objects cannot be created anymore without setting ``check_input = False``, as there is no certainty that its functions can handle them, and this should be fixed in a much diff --git a/src/sage/doctest/control.py b/src/sage/doctest/control.py index ed1dd5c8136..2fd11b3b3c9 100644 --- a/src/sage/doctest/control.py +++ b/src/sage/doctest/control.py @@ -23,7 +23,7 @@ import sage.misc.flatten from sage.structure.sage_object import SageObject from sage.env import DOT_SAGE, SAGE_LIB, SAGE_SRC -from sage.ext.c_lib import AlarmInterrupt, _init_csage +from sage.ext.interrupt import AlarmInterrupt, init_interrupts from sources import FileDocTestSource, DictAsObject from forker import DocTestDispatcher @@ -911,7 +911,7 @@ def run_val_gdb(self, testing=False): return # Setup Sage signal handler - _init_csage() + init_interrupts() import signal, subprocess p = subprocess.Popen(cmd, shell=True) diff --git a/src/c_lib/include/ccobject.h b/src/sage/ext/ccobject.h similarity index 97% rename from src/c_lib/include/ccobject.h rename to src/sage/ext/ccobject.h index 63a67d680e3..c44bf3ff861 100644 --- a/src/c_lib/include/ccobject.h +++ b/src/sage/ext/ccobject.h @@ -14,7 +14,7 @@ * @author Joel B. Mohler * * @brief These functions provide assistance for constructing and destructing - * C++ objects from pyrex. + * C++ objects from Cython. * */ @@ -108,6 +108,4 @@ PyObject* _to_PyString(const T *x) #endif -#endif // ndef __SAGE_CCOBJECT_H__ - - +#endif /* ifndef __SAGE_CCOBJECT_H__ */ diff --git a/src/sage/ext/interrupt.pxd b/src/sage/ext/interrupt.pxd new file mode 100644 index 00000000000..2f5465f86bc --- /dev/null +++ b/src/sage/ext/interrupt.pxd @@ -0,0 +1,28 @@ +# +# See c_lib/include/interrupt.h +# +cdef extern from 'interrupt.h': + int sig_on() nogil except 0 + int sig_str(char*) nogil except 0 + int sig_check() nogil except 0 + int sig_on_no_except() nogil + int sig_str_no_except(char*) nogil + int sig_check_no_except() nogil + void sig_off() nogil + void sig_retry() nogil # Does not return + void sig_error() nogil # Does not return + void sig_block() nogil + void sig_unblock() nogil + void setup_sage_signal_handler() nogil + void set_sage_signal_handler_message(char* s) nogil + void cython_check_exception() nogil except * + + ctypedef struct sage_signals_t: + int sig_on_count + int interrupt_received + int inside_signal_handler + int block_sigint + char* s + int (*raise_exception)(int sig, const char* msg) except 0 + + sage_signals_t _signals diff --git a/src/sage/ext/interrupt.pxi b/src/sage/ext/interrupt.pxi index 8997e777741..3e9d3829d79 100644 --- a/src/sage/ext/interrupt.pxi +++ b/src/sage/ext/interrupt.pxi @@ -1,27 +1 @@ -# -# See c_lib/include/interrupt.h -# -cdef extern from 'interrupt.h': - int sig_on() nogil except 0 - int sig_str(char*) nogil except 0 - int sig_check() nogil except 0 - int sig_on_no_except() nogil - int sig_str_no_except(char*) nogil - int sig_check_no_except() nogil - void sig_off() nogil - void sig_retry() nogil # Does not return - void sig_error() nogil # Does not return - void sig_block() nogil - void sig_unblock() nogil - void set_sage_signal_handler_message(char* s) nogil - void cython_check_exception() nogil except * - - ctypedef struct sage_signals_t: - int sig_on_count - int interrupt_received - int inside_signal_handler - int block_sigint - char* s - int (*raise_exception)(int sig, const char* msg) except 0 - - sage_signals_t _signals +from sage.ext.interrupt cimport * diff --git a/src/sage/ext/c_lib.pyx b/src/sage/ext/interrupt.pyx similarity index 80% rename from src/sage/ext/c_lib.pyx rename to src/sage/ext/interrupt.pyx index d01a50d0246..20139a6884c 100644 --- a/src/sage/ext/c_lib.pyx +++ b/src/sage/ext/interrupt.pyx @@ -1,9 +1,7 @@ r""" -Interface between Python and c_lib. - -This allows Python code to access a few parts of c_lib. This is not -needed for Cython code, since such code can access c_lib directly. +Cython interface to the interrupt handling code +See ``src/sage/tests/interrupt.pyx`` for extensive tests. AUTHORS: @@ -19,10 +17,10 @@ AUTHORS: # http://www.gnu.org/licenses/ #***************************************************************************** -include 'sage/ext/stdsage.pxi' include 'sage/ext/interrupt.pxi' -include 'sage/ext/cdefs.pxi' include 'sage/ext/signals.pxi' +from libc.stdio cimport freopen, stdin + class AlarmInterrupt(KeyboardInterrupt): """ @@ -34,7 +32,7 @@ class AlarmInterrupt(KeyboardInterrupt): Traceback (most recent call last): ... AlarmInterrupt - sage: from sage.ext.c_lib import do_raise_exception + sage: from sage.ext.interrupt import do_raise_exception sage: import signal sage: do_raise_exception(signal.SIGALRM) Traceback (most recent call last): @@ -50,7 +48,7 @@ class SignalError(BaseException): EXAMPLES:: - sage: from sage.ext.c_lib import do_raise_exception + sage: from sage.ext.interrupt import do_raise_exception sage: import signal sage: do_raise_exception(signal.SIGSEGV) Traceback (most recent call last): @@ -66,15 +64,13 @@ cdef int sig_raise_exception(int sig, const char* msg) except 0: """ if sig == SIGHUP or sig == SIGTERM: # Redirect stdin from /dev/null to close interactive sessions - freopen("/dev/null", "r", stdin); + freopen("/dev/null", "r", stdin) # This causes Python to exit raise SystemExit if sig == SIGINT: raise KeyboardInterrupt if sig == SIGALRM: - if msg == NULL: - msg = "" - raise AlarmInterrupt(msg) + raise AlarmInterrupt if sig == SIGILL: if msg == NULL: msg = "Illegal instruction" @@ -93,7 +89,7 @@ cdef int sig_raise_exception(int sig, const char* msg) except 0: raise SignalError(msg) if sig == SIGSEGV: if msg == NULL: - msg = "Segmentation fault"; + msg = "Segmentation fault" raise SignalError(msg) raise SystemError("unknown signal number %s"%sig) @@ -104,7 +100,7 @@ def do_raise_exception(sig, msg=None): EXAMPLES:: - sage: from sage.ext.c_lib import do_raise_exception + sage: from sage.ext.interrupt import do_raise_exception sage: import signal sage: do_raise_exception(signal.SIGFPE) Traceback (most recent call last): @@ -118,6 +114,17 @@ def do_raise_exception(sig, msg=None): Traceback (most recent call last): ... SystemError: unknown signal number 0 + + For interrupts, the message is ignored, see :trac:`17949`:: + + sage: do_raise_exception(signal.SIGINT, "ignored") + Traceback (most recent call last): + ... + KeyboardInterrupt + sage: do_raise_exception(signal.SIGALRM, "ignored") + Traceback (most recent call last): + ... + AlarmInterrupt """ cdef const char* m if msg is None: @@ -127,12 +134,12 @@ def do_raise_exception(sig, msg=None): sig_raise_exception(sig, m) -def _init_csage(): +def init_interrupts(): """ - Call init_csage() and enable interrupts. + Initialize the Sage interrupt framework. - This is normally done exactly once during Sage startup from - sage/all.py + This is normally done exactly once during Sage startup when + importing this module. """ # Set the Python-level interrupt handler. When a SIGINT occurs, # this will not be called directly. Instead, a SIGINT is caught by @@ -145,18 +152,18 @@ def _init_csage(): import signal signal.signal(signal.SIGINT, sage_python_check_interrupt) - init_csage() + setup_sage_signal_handler() _signals.raise_exception = sig_raise_exception -def _sig_on_reset(): +def sig_on_reset(): """ Return the current value of ``_signals.sig_on_count`` and set its value to zero. This is used by the doctesting framework. EXAMPLES:: - sage: from sage.ext.c_lib import _sig_on_reset as sig_on_reset + sage: from sage.ext.interrupt import sig_on_reset sage: cython('sig_on()'); sig_on_reset() 1 sage: sig_on_reset() @@ -174,3 +181,6 @@ def sage_python_check_interrupt(sig, frame): libcsage (c_lib). """ sig_check() + + +init_interrupts() diff --git a/src/sage/ext/python_rich_object.pxi b/src/sage/ext/python_rich_object.pxi index 0eed85a7087..bfd275dc9c6 100644 --- a/src/sage/ext/python_rich_object.pxi +++ b/src/sage/ext/python_rich_object.pxi @@ -1,7 +1,6 @@ from cpython.ref cimport PyObject, PyTypeObject cdef extern from "Python.h": - ctypedef void _typeobject ctypedef void (*freefunc)(void *) ctypedef void (*destructor)(PyObject *) ctypedef PyObject *(*getattrfunc)(PyObject *, char *) @@ -17,12 +16,14 @@ cdef extern from "Python.h": ctypedef PyObject *(*descrgetfunc) (PyObject *, PyObject *, PyObject *) ctypedef int (*descrsetfunc) (PyObject *, PyObject *, PyObject *) ctypedef int (*initproc)(PyObject *, PyObject *, PyObject *) - ctypedef PyObject *(*newfunc)(PyTypeObject *, PyObject *, PyObject *) + ctypedef object (*newfunc)(PyTypeObject *, PyObject *, PyObject *) ctypedef PyObject *(*allocfunc)(PyTypeObject *, Py_ssize_t) # We need a PyTypeObject with elements so we can # get and set tp_new, tp_dealloc, tp_flags, and tp_basicsize ctypedef struct RichPyTypeObject "PyTypeObject": + long tp_dictoffset + allocfunc tp_alloc newfunc tp_new freefunc tp_free diff --git a/src/sage/ext/stdsage.pxd b/src/sage/ext/stdsage.pxd new file mode 100644 index 00000000000..2a7ee094b3f --- /dev/null +++ b/src/sage/ext/stdsage.pxd @@ -0,0 +1,39 @@ +""" +Standard C helper code for Cython modules +""" +#***************************************************************************** +# Copyright (C) 2015 Jeroen Demeyer +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# http://www.gnu.org/licenses/ +#***************************************************************************** + +include 'python_rich_object.pxi' +from cpython.ref cimport Py_TYPE + + +cdef inline PY_NEW(type t): + """ + Return ``t.__new__(t)``. This works even for types like + :class:`Integer` where we change ``tp_new`` at runtime (Cython + optimizations assume that ``tp_new`` doesn't change). + """ + return (t).tp_new(t, NULL, NULL) + + +cdef inline void PY_SET_TP_NEW(type dst, type src): + """ + Manually set ``dst.__new__`` to ``src.__new__``. This is used to + speed up Cython's boilerplate object construction code by skipping + irrelevant base class ``tp_new`` methods. + """ + (dst).tp_new = (src).tp_new + + +cdef inline bint HAS_DICTIONARY(obj): + """ + Test whether the given object has a Python dictionary. + """ + return (Py_TYPE(obj)).tp_dictoffset != 0 diff --git a/src/sage/ext/stdsage.pxi b/src/sage/ext/stdsage.pxi index 5aa2a9d8ed8..b93074ca139 100644 --- a/src/sage/ext/stdsage.pxi +++ b/src/sage/ext/stdsage.pxi @@ -1,14 +1,8 @@ """ Standard C helper code for Cython modules - -Standard useful stuff for Sage Cython modules to include: -See stdsage.h for macros and stdsage.c for C functions. - -Each module currently gets its own copy of this, which is why -we call the initialization code below. """ #***************************************************************************** -# Copyright (C) 2005, 2006 William Stein +# Copyright (C) 2015 Jeroen Demeyer # # Distributed under the terms of the GNU General Public License (GPL) # as published by the Free Software Foundation; either version 2 of @@ -16,22 +10,6 @@ we call the initialization code below. # http://www.gnu.org/licenses/ #***************************************************************************** -cdef extern from "stdsage.h": - # Global tuple -- useful optimization - void init_global_empty_tuple() - object PY_NEW(object t) - object PY_NEW_SAME_TYPE(object t) - - void* PY_TYPE(object o) - bint PY_TYPE_CHECK(object o, object t) - bint PY_TYPE_CHECK_EXACT(object o, object t) - - object IS_INSTANCE(object o, object t) - void PY_SET_TP_NEW(object t1, object t2) - bint HAS_DICTIONARY(object o) - bint PY_IS_NUMERIC(object o) - - void init_csage() - +from sage.ext.stdsage cimport PY_NEW, HAS_DICTIONARY from sage.ext.memory cimport sage_free, sage_realloc, sage_malloc, sage_calloc from sage.ext.memory import init_memory_functions diff --git a/src/sage/geometry/all.py b/src/sage/geometry/all.py index c9a06855371..a4552a41b32 100644 --- a/src/sage/geometry/all.py +++ b/src/sage/geometry/all.py @@ -23,5 +23,9 @@ import toric_plotter + +from hyperbolic_space.all import * + lazy_import('sage.geometry.hyperplane_arrangement.arrangement', 'HyperplaneArrangements') lazy_import('sage.geometry.hyperplane_arrangement.library', 'hyperplane_arrangements') + diff --git a/src/sage/geometry/fan.py b/src/sage/geometry/fan.py index c0e51414526..ca3f5e4d5ff 100644 --- a/src/sage/geometry/fan.py +++ b/src/sage/geometry/fan.py @@ -1614,6 +1614,67 @@ def __neg__(self): self_cones = [cone.ambient_ray_indices() for cone in self] return RationalPolyhedralFan(self_cones, new_rays, self.lattice()) + def common_refinement(self, other): + """ + Return the common refinement of this fan and ``other``. + + INPUT: + + - ``other`` -- a :class:`fan ` in the same + :meth:`lattice` and with the same support as this fan + + OUTPUT: + + - a :class:`fan ` + + EXAMPLES: + + Refining a fan with itself gives itself:: + + sage: F0 = Fan2d([(1,0),(0,1),(-1,0),(0,-1)]) + sage: F0.common_refinement(F0) == F0 + True + + A more complex example with complete fans:: + + sage: F1 = Fan([[0],[1]],[(1,),(-1,)]) + sage: F2 = Fan2d([(1,0),(1,1),(0,1),(-1,0),(0,-1)]) + sage: F3 = F2.cartesian_product(F1) + sage: F4 = F1.cartesian_product(F2) + sage: FF = F3.common_refinement(F4) + sage: F3.ngenerating_cones() + 10 + sage: F4.ngenerating_cones() + 10 + sage: FF.ngenerating_cones() + 13 + + An example with two non-complete fans with the same support:: + + sage: F5 = Fan2d([(1,0),(1,2),(0,1)]) + sage: F6 = Fan2d([(1,0),(2,1),(0,1)]) + sage: F5.common_refinement(F6).ngenerating_cones() + 3 + + Both fans must live in the same lattice:: + + sage: F0.common_refinement(F1) + Traceback (most recent call last): + ... + ValueError: the fans are not in the same lattice + """ + from sage.categories.homset import End + from sage.geometry.fan_morphism import FanMorphism + N = self.lattice() + if other.lattice() is not N: + raise ValueError('the fans are not in the same lattice') + id = End(N).identity() + subdivision = FanMorphism(id, self, other, subdivide=True).domain_fan() + if not self.is_complete(): + # Construct the opposite morphism to ensure support equality + FanMorphism(id, other, self, subdivide=True) + return subdivision + def _latex_(self): r""" Return a LaTeX representation of ``self``. diff --git a/src/sage/geometry/hyperbolic_space/__init__.py b/src/sage/geometry/hyperbolic_space/__init__.py new file mode 100644 index 00000000000..c9fecacd721 --- /dev/null +++ b/src/sage/geometry/hyperbolic_space/__init__.py @@ -0,0 +1 @@ +import all diff --git a/src/sage/geometry/hyperbolic_space/all.py b/src/sage/geometry/hyperbolic_space/all.py new file mode 100644 index 00000000000..d1b2a930e04 --- /dev/null +++ b/src/sage/geometry/hyperbolic_space/all.py @@ -0,0 +1,3 @@ +from sage.misc.lazy_import import lazy_import + +lazy_import('sage.geometry.hyperbolic_space.hyperbolic_interface', 'HyperbolicPlane') diff --git a/src/sage/geometry/hyperbolic_space/hyperbolic_coercion.py b/src/sage/geometry/hyperbolic_space/hyperbolic_coercion.py new file mode 100644 index 00000000000..15b1d8ea289 --- /dev/null +++ b/src/sage/geometry/hyperbolic_space/hyperbolic_coercion.py @@ -0,0 +1,726 @@ +""" +Coercion Maps Between Hyperbolic Plane Models + +This module implements the coercion maps between different hyperbolic +plane models. + +AUTHORS: + +- Travis Scrimshaw (2014): initial version +""" + +#*********************************************************************** +# Copyright (C) 2014 Travis Scrimshaw +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# http://www.gnu.org/licenses/ +#*********************************************************************** + +from sage.categories.morphism import Morphism +from sage.symbolic.pynac import I +from sage.matrix.constructor import matrix +from sage.modules.free_module_element import vector +from sage.rings.integer import Integer +from sage.rings.infinity import infinity +from sage.functions.other import real, imag, sqrt +from sage.misc.lazy_import import lazy_import +lazy_import('sage.misc.misc', 'attrcall') + +class HyperbolicModelCoercion(Morphism): + """ + Abstract base class for morphisms between the hyperbolic models. + """ + def _repr_type(self): + """ + Return the type of morphism. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: PD = HyperbolicPlane().PD() + sage: phi = UHP.coerce_map_from(PD) + sage: phi._repr_type() + 'Coercion Isometry' + """ + return "Coercion Isometry" + + def _call_(self, x): + """ + Return the image of ``x`` under ``self``. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: PD = HyperbolicPlane().PD() + sage: HM = HyperbolicPlane().HM() + sage: phi = UHP.coerce_map_from(PD) + sage: phi(PD.get_point(0.5+0.5*I)) + Point in UHP 2.00000000000000 + 1.00000000000000*I + sage: psi = HM.coerce_map_from(UHP) + sage: psi(UHP.get_point(I)) + Point in HM (0, 0, 1) + + It is an error to try to convert a boundary point to a model + that doesn't support boundary points:: + + sage: psi(UHP.get_point(infinity)) + Traceback (most recent call last): + ... + NotImplementedError: boundary points are not implemented for the Hyperboloid Model + + It is an error to try to convert a boundary point to a model + that doesn't support boundary points:: + + sage: psi(UHP(infinity)) + Traceback (most recent call last): + ... + NotImplementedError: boundary points are not implemented for the Hyperboloid Model + """ + C = self.codomain() + if not C.is_bounded() and self.domain().is_bounded() and x.is_boundary(): + msg = u"boundary points are not implemented for the {}" + raise NotImplementedError(msg.format(C.name())) + + y = self.image_coordinates(x.coordinates()) + if self.domain().is_bounded(): + bdry = x.is_boundary() + else: + bdry = C.boundary_point_in_model(y) + + return C.element_class(C, y, bdry, check=False, **x.graphics_options()) + + def convert_geodesic(self, x): + """ + Convert the geodesic ``x`` of the domain into a geodesic of + the codomain. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: PD = HyperbolicPlane().PD() + sage: phi = UHP.coerce_map_from(PD) + sage: phi.convert_geodesic(PD.get_geodesic(0.5+0.5*I, -I)) + Geodesic in UHP from 2.00000000000000 + 1.00000000000000*I to 0 + """ + return self.codomain().get_geodesic(self(x.start()), self(x.end()), + **x.graphics_options()) + + def convert_isometry(self, x): + """ + Convert the hyperbolic isometry ``x`` of the domain into an + isometry of the codomain. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: HM = HyperbolicPlane().HM() + sage: phi = HM.coerce_map_from(UHP) + sage: I2 = UHP.get_isometry(identity_matrix(2)) + sage: phi.convert_isometry(I2) + Isometry in HM + [1 0 0] + [0 1 0] + [0 0 1] + """ + C = self.codomain() + return C._Isometry(C, self.image_isometry_matrix(x._matrix), check=False) + + def __invert__(self): + """ + Return the inverse coercion of ``self``. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: PD = HyperbolicPlane().PD() + sage: phi = UHP.coerce_map_from(PD) + sage: ~phi + Coercion Isometry morphism: + From: Hyperbolic plane in the Upper Half Plane Model model + To: Hyperbolic plane in the Poincare Disk Model model + """ + return self.domain().coerce_map_from(self.codomain()) + +############ +# From UHP # +############ + +class CoercionUHPtoPD(HyperbolicModelCoercion): + """ + Coercion from the UHP to PD model. + """ + def image_coordinates(self, x): + """ + Return the image of the coordinates of the hyperbolic point ``x`` + under ``self``. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: PD = HyperbolicPlane().PD() + sage: phi = PD.coerce_map_from(UHP) + sage: phi.image_coordinates(I) + 0 + """ + if x == infinity: + return I + return (x - I) / (Integer(1) - I*x) + + def image_isometry_matrix(self, x): + """ + Return the image of the matrix of the hyperbolic isometry ``x`` + under ``self``. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: PD = HyperbolicPlane().PD() + sage: phi = PD.coerce_map_from(UHP) + sage: phi.image_isometry_matrix(identity_matrix(2)) + [1 0] + [0 1] + """ + if x.det() < 0: +# x = I * x + return matrix([[1,-I],[-I,1]]) * x * matrix([[1,I],[I,1]]).conjugate()/Integer(2) + return matrix([[1,-I],[-I,1]]) * x * matrix([[1,I],[I,1]])/Integer(2) + +class CoercionUHPtoKM(HyperbolicModelCoercion): + """ + Coercion from the UHP to KM model. + """ + def image_coordinates(self, x): + """ + Return the image of the coordinates of the hyperbolic point ``x`` + under ``self``. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: KM = HyperbolicPlane().KM() + sage: phi = KM.coerce_map_from(UHP) + sage: phi.image_coordinates(3 + I) + (6/11, 9/11) + """ + if x == infinity: + return (0, 1) + return ((2*real(x))/(real(x)**2 + imag(x)**2 + 1), + (real(x)**2 + imag(x)**2 - 1)/(real(x)**2 + imag(x)**2 + 1)) + + def image_isometry_matrix(self, x): + """ + Return the image of the matrix of the hyperbolic isometry ``x`` + under ``self``. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: KM = HyperbolicPlane().KM() + sage: phi = KM.coerce_map_from(UHP) + sage: phi.image_isometry_matrix(identity_matrix(2)) + [1 0 0] + [0 1 0] + [0 0 1] + """ + return SL2R_to_SO21(x) + +class CoercionUHPtoHM(HyperbolicModelCoercion): + """ + Coercion from the UHP to HM model. + """ + def image_coordinates(self, x): + """ + Return the image of the coordinates of the hyperbolic point ``x`` + under ``self``. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: HM = HyperbolicPlane().HM() + sage: phi = HM.coerce_map_from(UHP) + sage: phi.image_coordinates(3 + I) + (3, 9/2, 11/2) + """ + return vector((real(x)/imag(x), + (real(x)**2 + imag(x)**2 - 1)/(2*imag(x)), + (real(x)**2 + imag(x)**2 + 1)/(2*imag(x)))) + + def image_isometry_matrix(self, x): + """ + Return the image of the matrix of the hyperbolic isometry ``x`` + under ``self``. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: HM = HyperbolicPlane().HM() + sage: phi = HM.coerce_map_from(UHP) + sage: phi.image_isometry_matrix(identity_matrix(2)) + [1 0 0] + [0 1 0] + [0 0 1] + """ + return SL2R_to_SO21(x) + +########### +# From PD # +########### + +class CoercionPDtoUHP(HyperbolicModelCoercion): + """ + Coercion from the PD to UHP model. + """ + def image_coordinates(self, x): + """ + Return the image of the coordinates of the hyperbolic point ``x`` + under ``self``. + + EXAMPLES:: + + sage: PD = HyperbolicPlane().PD() + sage: UHP = HyperbolicPlane().UHP() + sage: phi = UHP.coerce_map_from(PD) + sage: phi.image_coordinates(0.5+0.5*I) + 2.00000000000000 + 1.00000000000000*I + sage: phi.image_coordinates(0) + I + sage: phi.image_coordinates(I) + +Infinity + sage: phi.image_coordinates(-I) + 0 + """ + if x == I: + return infinity + return (x + I)/(Integer(1) + I*x) + + def image_isometry_matrix(self, x): + """ + Return the image of the matrix of the hyperbolic isometry ``x`` + under ``self``. + + EXAMPLES: + + We check that orientation-reversing isometries behave as they + should:: + + sage: PD = HyperbolicPlane().PD() + sage: UHP = HyperbolicPlane().UHP() + sage: phi = UHP.coerce_map_from(PD) + sage: phi.image_isometry_matrix(matrix([[0,I],[I,0]])) + [-1 0] + [ 0 -1] + """ + from sage.geometry.hyperbolic_space.hyperbolic_isometry import HyperbolicIsometryPD + if not HyperbolicIsometryPD._orientation_preserving(x): + return matrix([[1,I],[I,1]]) * x * matrix([[1,-I],[-I,1]]).conjugate() / Integer(2) + return matrix([[1,I],[I,1]]) * x * matrix([[1,-I],[-I,1]]) / Integer(2) + +class CoercionPDtoKM(HyperbolicModelCoercion): + """ + Coercion from the PD to KM model. + """ + def image_coordinates(self, x): + """ + Return the image of the coordinates of the hyperbolic point ``x`` + under ``self``. + + EXAMPLES:: + + sage: PD = HyperbolicPlane().PD() + sage: KM = HyperbolicPlane().KM() + sage: phi = KM.coerce_map_from(PD) + sage: phi.image_coordinates(0.5+0.5*I) + (0.666666666666667, 0.666666666666667) + """ + return (2*real(x)/(Integer(1) + real(x)**2 + imag(x)**2), + 2*imag(x)/(Integer(1) + real(x)**2 + imag(x)**2)) + + def image_isometry_matrix(self, x): + """ + Return the image of the matrix of the hyperbolic isometry ``x`` + under ``self``. + + EXAMPLES:: + + sage: PD = HyperbolicPlane().PD() + sage: KM = HyperbolicPlane().KM() + sage: phi = KM.coerce_map_from(PD) + sage: phi.image_isometry_matrix(matrix([[0,I],[I,0]])) + [-1 0 0] + [ 0 1 0] + [ 0 0 -1] + """ + return SL2R_to_SO21(matrix(2, [1, I, I, 1]) * x * + matrix(2, [1, -I, -I, 1]) / Integer(2)) + + +class CoercionPDtoHM(HyperbolicModelCoercion): + """ + Coercion from the PD to HM model. + """ + def image_coordinates(self, x): + """ + Return the image of the coordinates of the hyperbolic point ``x`` + under ``self``. + + EXAMPLES:: + + sage: PD = HyperbolicPlane().PD() + sage: HM = HyperbolicPlane().HM() + sage: phi = HM.coerce_map_from(PD) + sage: phi.image_coordinates(0.5+0.5*I) + (2.00000000000000, 2.00000000000000, 3.00000000000000) + """ + return vector((2*real(x)/(1 - real(x)**2 - imag(x)**2), + 2*imag(x)/(1 - real(x)**2 - imag(x)**2), + (real(x)**2 + imag(x)**2 + 1) / + (1 - real(x)**2 - imag(x)**2))) + + def image_isometry_matrix(self, x): + """ + Return the image of the matrix of the hyperbolic isometry ``x`` + under ``self``. + + EXAMPLES:: + + sage: PD = HyperbolicPlane().PD() + sage: HM = HyperbolicPlane().HM() + sage: phi = HM.coerce_map_from(PD) + sage: phi.image_isometry_matrix(matrix([[0,I],[I,0]])) + [-1 0 0] + [ 0 1 0] + [ 0 0 -1] + """ + return SL2R_to_SO21(matrix(2, [1, I, I, 1]) * x * + matrix(2, [1, -I, -I, 1]) / Integer(2)) + +########### +# From KM # +########### + + +class CoercionKMtoUHP(HyperbolicModelCoercion): + """ + Coercion from the KM to UHP model. + """ + def image_coordinates(self, x): + """ + Return the image of the coordinates of the hyperbolic point ``x`` + under ``self``. + + EXAMPLES:: + + sage: KM = HyperbolicPlane().KM() + sage: UHP = HyperbolicPlane().UHP() + sage: phi = UHP.coerce_map_from(KM) + sage: phi.image_coordinates((0, 0)) + I + sage: phi.image_coordinates((0, 1)) + +Infinity + """ + if tuple(x) == (0, 1): + return infinity + return (-x[0]/(x[1] - 1) + + I*(-(sqrt(-x[0]**2 - x[1]**2 + 1) - x[0]**2 - x[1]**2 + 1) + / ((x[1] - 1)*sqrt(-x[0]**2 - x[1]**2 + 1) + x[1] - 1))) + + def image_isometry_matrix(self, x): + """ + Return the image of the matrix of the hyperbolic isometry ``x`` + under ``self``. + + EXAMPLES:: + + sage: KM = HyperbolicPlane().KM() + sage: UHP = HyperbolicPlane().UHP() + sage: phi = UHP.coerce_map_from(KM) + sage: m = matrix([[5/3,0,4/3], [0,1,0], [4/3,0,5/3]]) + sage: phi.image_isometry_matrix(m) + [2*sqrt(1/3) sqrt(1/3)] + [ sqrt(1/3) 2*sqrt(1/3)] + """ + return SO21_to_SL2R(x) + +class CoercionKMtoPD(HyperbolicModelCoercion): + """ + Coercion from the KM to PD model. + """ + def image_coordinates(self, x): + """ + Return the image of the coordinates of the hyperbolic point ``x`` + under ``self``. + + EXAMPLES:: + + sage: KM = HyperbolicPlane().KM() + sage: PD = HyperbolicPlane().PD() + sage: phi = PD.coerce_map_from(KM) + sage: phi.image_coordinates((0, 0)) + 0 + """ + return (x[0]/(1 + (1 - x[0]**2 - x[1]**2).sqrt()) + + I*x[1]/(1 + (1 - x[0]**2 - x[1]**2).sqrt())) + + def image_isometry_matrix(self, x): + """ + Return the image of the matrix of the hyperbolic isometry ``x`` + under ``self``. + + EXAMPLES:: + + sage: KM = HyperbolicPlane().KM() + sage: PD = HyperbolicPlane().PD() + sage: phi = PD.coerce_map_from(KM) + sage: m = matrix([[5/3,0,4/3], [0,1,0], [4/3,0,5/3]]) + sage: phi.image_isometry_matrix(m) + [2*sqrt(1/3) sqrt(1/3)] + [ sqrt(1/3) 2*sqrt(1/3)] + """ + return (matrix(2,[1,-I,-I,1]) * SO21_to_SL2R(x) * + matrix(2,[1,I,I,1])/Integer(2)) + +class CoercionKMtoHM(HyperbolicModelCoercion): + """ + Coercion from the KM to HM model. + """ + def image_coordinates(self, x): + """ + Return the image of the coordinates of the hyperbolic point ``x`` + under ``self``. + + EXAMPLES:: + + sage: KM = HyperbolicPlane().KM() + sage: HM = HyperbolicPlane().HM() + sage: phi = HM.coerce_map_from(KM) + sage: phi.image_coordinates((0, 0)) + (0, 0, 1) + """ + return (vector((2*x[0], 2*x[1], 1 + x[0]**2 + x[1]**2)) + / (1 - x[0]**2 - x[1]**2)) + + def image_isometry_matrix(self, x): + """ + Return the image of the matrix of the hyperbolic isometry ``x`` + under ``self``. + + EXAMPLES:: + + sage: KM = HyperbolicPlane().KM() + sage: HM = HyperbolicPlane().HM() + sage: phi = HM.coerce_map_from(KM) + sage: m = matrix([[5/3,0,4/3], [0,1,0], [4/3,0,5/3]]) + sage: phi.image_isometry_matrix(m) + [5/3 0 4/3] + [ 0 1 0] + [4/3 0 5/3] + """ + return x + +########### +# From HM # +########### + +class CoercionHMtoUHP(HyperbolicModelCoercion): + """ + Coercion from the HM to UHP model. + """ + def image_coordinates(self, x): + """ + Return the image of the coordinates of the hyperbolic point ``x`` + under ``self``. + + EXAMPLES:: + + sage: HM = HyperbolicPlane().HM() + sage: UHP = HyperbolicPlane().UHP() + sage: phi = UHP.coerce_map_from(HM) + sage: phi.image_coordinates( vector((0,0,1)) ) + I + """ + return -((x[0]*x[2] + x[0]) + I*(x[2] + 1)) / ((x[1] - 1)*x[2] + - x[0]**2 - x[1]**2 + x[1] - 1) + + def image_isometry_matrix(self, x): + """ + Return the image of the matrix of the hyperbolic isometry ``x`` + under ``self``. + + EXAMPLES:: + + sage: HM = HyperbolicPlane().HM() + sage: UHP = HyperbolicPlane().UHP() + sage: phi = UHP.coerce_map_from(HM) + sage: phi.image_isometry_matrix(identity_matrix(3)) + [1 0] + [0 1] + """ + return SO21_to_SL2R(x) + +class CoercionHMtoPD(HyperbolicModelCoercion): + """ + Coercion from the HM to PD model. + """ + def image_coordinates(self, x): + """ + Return the image of the coordinates of the hyperbolic point ``x`` + under ``self``. + + EXAMPLES:: + + sage: HM = HyperbolicPlane().HM() + sage: PD = HyperbolicPlane().PD() + sage: phi = PD.coerce_map_from(HM) + sage: phi.image_coordinates( vector((0,0,1)) ) + 0 + """ + return x[0]/(1 + x[2]) + I*(x[1]/(1 + x[2])) + + def image_isometry_matrix(self, x): + """ + Return the image of the matrix of the hyperbolic isometry ``x`` + under ``self``. + + EXAMPLES:: + + sage: HM = HyperbolicPlane().HM() + sage: PD = HyperbolicPlane().PD() + sage: phi = PD.coerce_map_from(HM) + sage: phi.image_isometry_matrix(identity_matrix(3)) + [1 0] + [0 1] + """ + return (matrix(2,[1,-I,-I,1]) * SO21_to_SL2R(x) * + matrix(2,[1,I,I,1])/Integer(2)) + +class CoercionHMtoKM(HyperbolicModelCoercion): + """ + Coercion from the HM to KM model. + """ + def image_coordinates(self, x): + """ + Return the image of the coordinates of the hyperbolic point ``x`` + under ``self``. + + EXAMPLES:: + + sage: HM = HyperbolicPlane().HM() + sage: KM = HyperbolicPlane().KM() + sage: phi = KM.coerce_map_from(HM) + sage: phi.image_coordinates( vector((0,0,1)) ) + (0, 0) + """ + return (x[0]/(1 + x[2]), x[1]/(1 + x[2])) + + def image_isometry_matrix(self, x): + """ + Return the image of the matrix of the hyperbolic isometry ``x`` + under ``self``. + + EXAMPLES:: + + sage: HM = HyperbolicPlane().HM() + sage: KM = HyperbolicPlane().KM() + sage: phi = KM.coerce_map_from(HM) + sage: phi.image_isometry_matrix(identity_matrix(3)) + [1 0 0] + [0 1 0] + [0 0 1] + """ + return x + +##################################################################### +## Helper functions + +def SL2R_to_SO21(A): + r""" + Given a matrix in `SL(2, \RR)` return its irreducible representation in + `O(2,1)`. + + Note that this is not the only homomorphism, but it is the only one + that works in the context of the implemented 2D hyperbolic geometry + models. + + EXAMPLES:: + + sage: from sage.geometry.hyperbolic_space.hyperbolic_coercion import SL2R_to_SO21 + sage: A = SL2R_to_SO21(identity_matrix(2)) + sage: J = matrix([[1,0,0],[0,1,0],[0,0,-1]]) #Lorentzian Gram matrix + sage: norm(A.transpose()*J*A - J) < 10**-4 + True + """ + a, b, c, d = (A/A.det().sqrt()).list() + + # Kill ~0 imaginary parts + B = matrix(3, map(real, + [a*d + b*c, a*c - b*d, a*c + b*d, a*b - c*d, + Integer(1)/Integer(2)*a**2 - Integer(1)/Integer(2)*b**2 - + Integer(1)/Integer(2)*c**2 + Integer(1)/Integer(2)*d**2, + Integer(1)/Integer(2)*a**2 + Integer(1)/Integer(2)*b**2 - + Integer(1)/Integer(2)*c**2 - Integer(1)/Integer(2)*d**2, + a*b + c*d, Integer(1)/Integer(2)*a**2 - + Integer(1)/Integer(2)*b**2 + Integer(1)/Integer(2)*c**2 - + Integer(1)/Integer(2)*d**2, Integer(1)/Integer(2)*a**2 + + Integer(1)/Integer(2)*b**2 + Integer(1)/Integer(2)*c**2 + + Integer(1)/Integer(2)*d**2])) + + #B = B.apply_map(attrcall('real')) + if A.det() > 0: + return B + else: + # Orientation-reversing isometries swap the nappes of + # the lightcone. This fixes that issue. + return -B + +def SO21_to_SL2R(M): + r""" + A homomorphism from `SO(2, 1)` to `SL(2, \RR)`. + + Note that this is not the only homomorphism, but it is the only one + that works in the context of the implemented 2D hyperbolic geometry + models. + + EXAMPLES:: + + sage: from sage.geometry.hyperbolic_space.hyperbolic_coercion import SO21_to_SL2R + sage: (SO21_to_SL2R(identity_matrix(3)) - identity_matrix(2)).norm() < 10**-4 + True + """ + #################################################################### + # SL(2,R) is the double cover of SO (2,1)^+, so we need to choose # + # a lift. I have formulas for the absolute values of each entry # + # a,b ,c,d of the lift matrix(2,[a,b,c,d]), but we need to choose # + # one entry to be positive. I choose d for no particular reason, # + # unless d = 0, then we choose c > 0. The basic strategy for this # + # function is to find the linear map induced by the SO(2,1) # + # element on the Lie algebra sl(2, R). This corresponds to the # + # Adjoint action by a matrix A or -A in SL(2,R). To find which # + # matrix let X,Y,Z be a basis for sl(2,R) and look at the images # + # of X,Y,Z as well as the second and third standard basis vectors # + # for 2x2 matrices (these are traceless, so are in the Lie # + # algebra). These corresponds to AXA^-1 etc and give formulas # + # for the entries of A. # + #################################################################### + (m_1,m_2,m_3,m_4,m_5,m_6,m_7,m_8,m_9) = M.list() + d = sqrt(Integer(1)/Integer(2)*m_5 - Integer(1)/Integer(2)*m_6 - + Integer(1)/Integer(2)*m_8 + Integer(1)/Integer(2)*m_9) + if M.det() > 0: # EPSILON? + det_sign = 1 + elif M.det() < 0: # EPSILON? + det_sign = -1 + if d > 0: # EPSILON? + c = (-Integer(1)/Integer(2)*m_4 + Integer(1)/Integer(2)*m_7)/d + b = (-Integer(1)/Integer(2)*m_2 + Integer(1)/Integer(2)*m_3)/d + ad = det_sign*1 + b*c # ad - bc = pm 1 + a = ad/d + else: # d is 0, so we make c > 0 + c = sqrt(-Integer(1)/Integer(2)*m_5 - Integer(1)/Integer(2)*m_6 + + Integer(1)/Integer(2)*m_8 + Integer(1)/Integer(2)*m_9) + d = (-Integer(1)/Integer(2)*m_4 + Integer(1)/Integer(2)*m_7)/c + # d = 0, so ad - bc = -bc = pm 1. + b = - (det_sign*1)/c + a = (Integer(1)/Integer(2)*m_4 + Integer(1)/Integer(2)*m_7)/b + A = matrix(2, [a, b, c, d]) + return A diff --git a/src/sage/geometry/hyperbolic_space/hyperbolic_constants.py b/src/sage/geometry/hyperbolic_space/hyperbolic_constants.py new file mode 100644 index 00000000000..1ea800e58b0 --- /dev/null +++ b/src/sage/geometry/hyperbolic_space/hyperbolic_constants.py @@ -0,0 +1,4 @@ +from sage.matrix.constructor import matrix + +EPSILON = 10 ** -9 +LORENTZ_GRAM = matrix(3, [1, 0, 0, 0, 1, 0, 0, 0, -1]) diff --git a/src/sage/geometry/hyperbolic_space/hyperbolic_geodesic.py b/src/sage/geometry/hyperbolic_space/hyperbolic_geodesic.py new file mode 100644 index 00000000000..709370e0bec --- /dev/null +++ b/src/sage/geometry/hyperbolic_space/hyperbolic_geodesic.py @@ -0,0 +1,1369 @@ +# -*- coding: utf-8 -*- +r""" +Hyperbolic Geodesics + +This module implements the abstract base class for geodesics in +hyperbolic space of arbitrary dimension. It also contains the +implementations for specific models of hyperbolic geometry. + +AUTHORS: + +- Greg Laun (2013): initial version + +EXAMPLES: + +We can construct geodesics in the upper half plane model, abbreviated +UHP for convenience:: + + sage: HyperbolicPlane().UHP().get_geodesic(2, 3) + Geodesic in UHP from 2 to 3 + sage: g = HyperbolicPlane().UHP().get_geodesic(I, 3 + I) + sage: g.length() + arccosh(11/2) + +Geodesics are oriented, which means that two geodesics with the same +graph will only be equal if their starting and ending points are +the same:: + + sage: HyperbolicPlane().UHP().get_geodesic(1,2) == HyperbolicPlane().UHP().get_geodesic(2,1) + False + +.. TODO:: + + Implement a parent for all geodesics of the hyperbolic plane? +""" + + +#*********************************************************************** +# Copyright (C) 2013 Greg Laun +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# http://www.gnu.org/licenses/ +#*********************************************************************** + +from sage.structure.sage_object import SageObject +from sage.symbolic.pynac import I +from sage.misc.lazy_attribute import lazy_attribute +from sage.rings.infinity import infinity +from sage.rings.all import CC, RR +from sage.symbolic.constants import pi +from sage.modules.free_module_element import vector +from sage.matrix.constructor import matrix +from sage.functions.other import real, imag, sqrt +from sage.functions.trig import sin, cos, arccos +from sage.functions.log import exp +from sage.functions.hyperbolic import sinh, cosh, arcsinh + +from sage.geometry.hyperbolic_space.hyperbolic_constants import EPSILON + +from sage.misc.lazy_import import lazy_import +lazy_import('sage.geometry.hyperbolic_space.hyperbolic_isometry', + 'mobius_transform') + + +class HyperbolicGeodesic(SageObject): + r""" + Abstract base class for oriented geodesics that are not necessarily + complete. + + INPUT: + + - ``start`` -- a HyperbolicPoint or coordinates of a point in + hyperbolic space representing the start of the geodesic + + - ``end`` -- a HyperbolicPoint or coordinates of a point in + hyperbolic space representing the end of the geodesic + + EXAMPLES:: + + sage: HyperbolicPlane().UHP().get_geodesic(1, 0) + Geodesic in UHP from 1 to 0 + + sage: HyperbolicPlane().PD().get_geodesic(1, 0) + Geodesic in PD from 1 to 0 + + sage: HyperbolicPlane().KM().get_geodesic((0,1/2), (1/2, 0)) + Geodesic in KM from (0, 1/2) to (1/2, 0) + + sage: HyperbolicPlane().HM().get_geodesic((0,0,1), (0,1, sqrt(2))) + Geodesic in HM from (0, 0, 1) to (0, 1, sqrt(2)) + """ + + ##################### + # "Private" Methods # + ##################### + + def __init__(self, model, start, end, **graphics_options): + r""" + See :class:`HyperbolicGeodesic` for full documentation. + + EXAMPLES :: + + sage: HyperbolicPlane().UHP().get_geodesic(I, 2 + I) + Geodesic in UHP from I to I + 2 + """ + self._model = model + self._start = start + self._end = end + self._graphics_options = graphics_options + + @lazy_attribute + def _cached_geodesic(self): + r""" + The representation of the geodesic used for calculations. + + EXAMPLES:: + + sage: A = HyperbolicPlane().PD().get_geodesic(0, 1/2) + sage: A._cached_geodesic + Geodesic in UHP from I to 3/5*I + 4/5 + """ + M = self._model.realization_of().a_realization() + return self.to_model(M) + + @lazy_attribute + def _complete(self): + r""" + Return whether the geodesic is complete. This is used for + geodesics in non-bounded models. For thse models, + ``self.complete()`` simply sets ``_complete`` to ``True``. + + EXAMPLES:: + + sage: HyperbolicPlane().UHP().get_geodesic(1, -12)._complete + True + sage: HyperbolicPlane().UHP().get_geodesic(I, 2 + I)._complete + False + sage: g = HyperbolicPlane().HM().get_geodesic((0,0,1), (0,1, sqrt(2))) + sage: g._complete + False + sage: g.complete()._complete + True + """ + if self._model.is_bounded(): + return (self._start.is_boundary() and self._end.is_boundary()) + return False #All non-bounded geodesics start life incomplete. + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLES:: + + sage: HyperbolicPlane().UHP().get_geodesic(3 + 4*I, I) + Geodesic in UHP from 4*I + 3 to I + + sage: HyperbolicPlane().PD().get_geodesic(1/2 + I/2, 0) + Geodesic in PD from 1/2*I + 1/2 to 0 + + sage: HyperbolicPlane().KM().get_geodesic((1/2, 1/2), (0, 0)) + Geodesic in KM from (1/2, 1/2) to (0, 0) + + sage: HyperbolicPlane().HM().get_geodesic((0,0,1), (0, 1, sqrt(Integer(2)))) + Geodesic in HM from (0, 0, 1) to (0, 1, sqrt(2)) + """ + return "Geodesic in {0} from {1} to {2}".format(self._model.short_name(), + self._start.coordinates(), self._end.coordinates()) + + def __eq__(self, other): + r""" + Return ``True`` if ``self`` is equal to other as an oriented geodesic. + + EXAMPLES:: + + sage: g1 = HyperbolicPlane().UHP().get_geodesic(I, 2*I) + sage: g2 = HyperbolicPlane().UHP().get_geodesic(2*I,I) + sage: g1 == g2 + False + sage: g1 == g1 + True + """ + if not isinstance(other, HyperbolicGeodesic): + return False + return (self._model is other._model + and self._start == other._start + and self._end == other._end) + + ####################### + # Setters and Getters # + ####################### + + def start(self): + r""" + Return the starting point of the geodesic. + + EXAMPLES:: + + sage: g = HyperbolicPlane().UHP().get_geodesic(I, 3*I) + sage: g.start() + Point in UHP I + """ + return self._start + + def end(self): + r""" + Return the starting point of the geodesic. + + EXAMPLES:: + + sage: g = HyperbolicPlane().UHP().get_geodesic(I, 3*I) + sage: g.end() + Point in UHP 3*I + """ + return self._end + + def endpoints(self): + r""" + Return a list containing the start and endpoints. + + EXAMPLES:: + + sage: g = HyperbolicPlane().UHP().get_geodesic(I, 3*I) + sage: g.endpoints() + [Point in UHP I, Point in UHP 3*I] + """ + return [self._start, self._end] + + def model(self): + r""" + Return the model to which the :class:`HyperbolicGeodesic` belongs. + + EXAMPLES:: + + sage: HyperbolicPlane().UHP().get_geodesic(I, 2*I).model() + Hyperbolic plane in the Upper Half Plane Model model + + sage: HyperbolicPlane().PD().get_geodesic(0, I/2).model() + Hyperbolic plane in the Poincare Disk Model model + + sage: HyperbolicPlane().KM().get_geodesic((0, 0), (0, 1/2)).model() + Hyperbolic plane in the Klein Disk Model model + + sage: HyperbolicPlane().HM().get_geodesic((0, 0, 1), (0, 1, sqrt(2))).model() + Hyperbolic plane in the Hyperboloid Model model + """ + return self._model + + def to_model(self, model): + r""" + Convert the current object to image in another model. + + INPUT: + + - ``model`` -- the image model + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: PD = HyperbolicPlane().PD() + sage: UHP.get_geodesic(I, 2*I).to_model(PD) + Geodesic in PD from 0 to 1/3*I + sage: UHP.get_geodesic(I, 2*I).to_model('PD') + Geodesic in PD from 0 to 1/3*I + """ + if isinstance(model, str): + model = getattr(self._model.realization_of(), model)() + if not model.is_bounded() and self.length() == infinity: + raise NotImplementedError("cannot convert to an unbounded model") + start = model(self._start) + end = model(self._end) + g = model.get_geodesic(start, end) + return g + + def graphics_options(self): + r""" + Return the graphics options of ``self``. + + EXAMPLES:: + + sage: g = HyperbolicPlane().UHP().get_geodesic(I, 2*I, color="red") + sage: g.graphics_options() + {'color': 'red'} + """ + return self._graphics_options + + def update_graphics(self, update=False, **options): + r""" + Update the graphics options of ``self``. + + INPUT: + + - ``update`` -- if ``True``, the original option are updated + rather than overwritten + + EXAMPLES:: + + sage: g = HyperbolicPlane().UHP().get_geodesic(I, 2*I) + sage: g.graphics_options() + {} + + sage: g.update_graphics(color = "red"); g.graphics_options() + {'color': 'red'} + + sage: g.update_graphics(color = "blue"); g.graphics_options() + {'color': 'blue'} + + sage: g.update_graphics(True, size = 20); g.graphics_options() + {'color': 'blue', 'size': 20} + """ + if not update: + self._graphics_options = {} + self._graphics_options.update(**options) + + ################### + # Boolean Methods # + ################### + + def is_complete(self): + r""" + Return ``True`` if ``self`` is a complete geodesic (that is, both + endpoints are on the ideal boundary) and ``False`` otherwise. + + EXAMPLES:: + + sage: HyperbolicPlane().UHP().get_geodesic(I, 2*I).is_complete() + False + + sage: HyperbolicPlane().UHP().get_geodesic(0, I).is_complete() + False + + sage: HyperbolicPlane().UHP().get_geodesic(0, infinity).is_complete() + True + """ + return self._complete + + def is_asymptotically_parallel(self, other): + r""" + Return ``True`` if ``self`` and ``other`` are asymptotically + parallel and ``False`` otherwise. + + INPUT: + + - ``other`` -- a hyperbolic geodesic + + EXAMPLES:: + + sage: g = HyperbolicPlane().UHP().get_geodesic(-2,5) + sage: h = HyperbolicPlane().UHP().get_geodesic(-2,4) + sage: g.is_asymptotically_parallel(h) + True + + Ultraparallel geodesics are not asymptotically parallel:: + + sage: g = HyperbolicPlane().UHP().get_geodesic(-2,5) + sage: h = HyperbolicPlane().UHP().get_geodesic(-1,4) + sage: g.is_asymptotically_parallel(h) + False + + No hyperbolic geodesic is asymptotically parallel to itself:: + + sage: g = HyperbolicPlane().UHP().get_geodesic(-2,5) + sage: g.is_asymptotically_parallel(g) + False + """ + p1, p2 = self.complete().endpoints() + q1, q2 = other.complete().endpoints() + return ((self != other) and ((p1 in [q1, q2]) or (p2 in [q1,q2])) + and self.model() is other.model()) + + def is_ultra_parallel(self, other): + r""" + Return ``True`` if ``self`` and ``other`` are ultra parallel + and ``False`` otherwise. + + INPUT: + + - ``other`` -- a hyperbolic geodesic + + EXAMPLES:: + + sage: from sage.geometry.hyperbolic_space.hyperbolic_geodesic import * + sage: g = HyperbolicPlane().UHP().get_geodesic(0,1) + sage: h = HyperbolicPlane().UHP().get_geodesic(-3,3) + sage: g.is_ultra_parallel(h) + True + + :: + + sage: g = HyperbolicPlane().UHP().get_geodesic(-2,5) + sage: h = HyperbolicPlane().UHP().get_geodesic(2,6) + sage: g.is_ultra_parallel(h) + False + + :: + + sage: g = HyperbolicPlane().UHP().get_geodesic(-2,5) + sage: g.is_ultra_parallel(g) + False + """ + A = self.reflection_involution() + B = other.reflection_involution() + return (A * B).classification() == 'hyperbolic' + + def is_parallel(self, other): + r""" + Return ``True`` if the two given hyperbolic geodesics are either + ultra parallel or asymptotically parallel and``False`` otherwise. + + INPUT: + + - ``other`` -- a hyperbolic geodesic in any model + + OUTPUT: + + ``True`` if the given geodesics are either ultra parallel or + asymptotically parallel, ``False`` if not. + + EXAMPLES:: + + sage: g = HyperbolicPlane().UHP().get_geodesic(-2,5) + sage: h = HyperbolicPlane().UHP().get_geodesic(5,12) + sage: g.is_parallel(h) + True + + :: + + sage: g = HyperbolicPlane().UHP().get_geodesic(-2,5) + sage: h = HyperbolicPlane().UHP().get_geodesic(-2,4) + sage: g.is_parallel(h) + True + + No hyperbolic geodesic is either ultra parallel or + asymptotically parallel to itself:: + + sage: g = HyperbolicPlane().UHP().get_geodesic(-2,5) + sage: g.is_parallel(g) + False + """ + A = self.reflection_involution() + B = other.reflection_involution() + return (A * B).classification() in ['parabolic', 'hyperbolic'] + + def ideal_endpoints(self): + r""" + Return the ideal endpoints in bounded models. Raise a + ``NotImplementedError`` in models that are not bounded. + + EXAMPLES:: + + sage: H = HyperbolicPlane() + sage: H.UHP().get_geodesic(1 + I, 1 + 3*I).ideal_endpoints() + [Boundary point in UHP 1, Boundary point in UHP +Infinity] + + sage: H.PD().get_geodesic(0, I/2).ideal_endpoints() + [Boundary point in PD -I, Boundary point in PD I] + + sage: H.KM().get_geodesic((0,0), (0, 1/2)).ideal_endpoints() + [Boundary point in KM (0, -1), Boundary point in KM (0, 1)] + + sage: H.HM().get_geodesic((0,0,1), (1, 0, sqrt(2))).ideal_endpoints() + Traceback (most recent call last): + ... + NotImplementedError: boundary points are not implemented in the HM model + """ + if not self._model.is_bounded(): + raise NotImplementedError("boundary points are not implemented in the " + + "{0} model".format(self._model.short_name())) + if self.is_complete(): + return self.endpoints() + return [self._model(k) for k in self._cached_geodesic.ideal_endpoints()] + + def complete(self): + r""" + Return the geodesic with ideal endpoints in bounded models. Raise a + ``NotImplementedError`` in models that are not bounded. + + EXAMPLES:: + + sage: H = HyperbolicPlane() + sage: H.UHP().get_geodesic(1 + I, 1 + 3*I).complete() + Geodesic in UHP from 1 to +Infinity + + sage: H.PD().get_geodesic(0, I/2).complete() + Geodesic in PD from -I to I + + sage: H.KM().get_geodesic((0,0), (0, 1/2)).complete() + Geodesic in KM from (0, -1) to (0, 1) + + sage: H.HM().get_geodesic((0,0,1), (1, 0, sqrt(2))).complete() + Geodesic in HM from (0, 0, 1) to (1, 0, sqrt(2)) + + sage: H.HM().get_geodesic((0,0,1), (1, 0, sqrt(2))).complete().is_complete() + True + """ + if self._model.is_bounded(): + return self._model.get_geodesic(*self.ideal_endpoints()) + + from copy import copy + g = copy(self) + g._complete = True + return g + + def reflection_involution(self): + r""" + Return the involution fixing ``self``. + + EXAMPLES:: + + sage: H = HyperbolicPlane() + sage: gU = H.UHP().get_geodesic(2,4) + sage: RU = gU.reflection_involution(); RU + Isometry in UHP + [ 3 -8] + [ 1 -3] + + sage: RU*gU == gU + True + + sage: gP = H.PD().get_geodesic(0, I) + sage: RP = gP.reflection_involution(); RP + Isometry in PD + [ 1 0] + [ 0 -1] + + sage: RP*gP == gP + True + + sage: gK = H.KM().get_geodesic((0,0), (0,1)) + sage: RK = gK.reflection_involution(); RK + Isometry in KM + [-1 0 0] + [ 0 1 0] + [ 0 0 1] + + sage: RK*gK == gK + True + + sage: A = H.HM().get_geodesic((0,0,1), (1,0, n(sqrt(2)))).reflection_involution() + sage: B = diagonal_matrix([1, -1, 1]) + sage: bool((B - A.matrix()).norm() < 10**-9) + True + + The above tests go through the Upper Half Plane. It remains to + test that the matrices in the models do what we intend. :: + + sage: from sage.geometry.hyperbolic_space.hyperbolic_isometry import mobius_transform + sage: R = H.PD().get_geodesic(-1,1).reflection_involution() + sage: bool(mobius_transform(R.matrix(), 0) == 0) + True + """ + return self._cached_geodesic.reflection_involution().to_model(self._model) + + def common_perpendicula(self, other): + r""" + Return the unique hyperbolic geodesic perpendicular to two given + geodesics, if such a geodesic exists. If none exists, raise a + ``ValueError``. + + INPUT: + + - ``other`` -- a hyperbolic geodesic in the same model as ``self`` + + OUTPUT: + + - a hyperbolic geodesic + + EXAMPLES:: + + sage: g = HyperbolicPlane().UHP().get_geodesic(2,3) + sage: h = HyperbolicPlane().UHP().get_geodesic(4,5) + sage: g.common_perpendicular(h) + Geodesic in UHP from 1/2*sqrt(3) + 7/2 to -1/2*sqrt(3) + 7/2 + + It is an error to ask for the common perpendicular of two + intersecting geodesics:: + + sage: g = HyperbolicPlane().UHP().get_geodesic(2,4) + sage: h = HyperbolicPlane().UHP().get_geodesic(3, infinity) + sage: g.common_perpendicular(h) + Traceback (most recent call last): + ... + ValueError: geodesics intersect; no common perpendicular exists + """ + if not self.is_parallel(other): + raise ValueError('geodesics intersect; no common perpendicular exists') + return self._cached_geodesic.common_perpendicular(other).to_model(self._model) + + def intersection(self, other): + r""" + Return the point of intersection of two geodesics (if such a + point exists). + + INPUT: + + - ``other`` -- a hyperbolic geodesic in the same model as ``self`` + + OUTPUT: + + - a hyperbolic point or geodesic + + EXAMPLES:: + + sage: PD = HyperbolicPlane().PD() + """ + if self == other: + return self + elif self.is_parallel(other): + raise ValueError("geodesics don't intersect") + inters = self._cached_geodesic.intersection(other) + if len(inters) == 2: + return self + elif len(inters) == 1: + return [self._model(inters[0])] + return [] + + def perpendicular_bisector(self): + r""" + Return the perpendicular bisector of ``self`` if ``self`` has + finite length. Here distance is hyperbolic distance. + + EXAMPLES:: + + sage: g = HyperbolicPlane().PD().random_geodesic() + sage: h = g.perpendicular_bisector() + sage: bool(h.intersection(g)[0].coordinates() - g.midpoint().coordinates() < 10**-9) + True + + Complete geodesics cannot be bisected:: + + sage: g = HyperbolicPlane().PD().get_geodesic(0, 1) + sage: g.perpendicular_bisector() + Traceback (most recent call last): + ... + ValueError: the length must be finite + """ + P = self._cached_geodesic.perpendicular_bisector() + return P.to_model(self._model) + + def midpoint(self): + r""" + Return the (hyperbolic) midpoint of a hyperbolic line segment. + + EXAMPLES:: + + sage: g = HyperbolicPlane().UHP().random_geodesic() + sage: m = g.midpoint() + sage: end1, end2 = g.endpoints() + sage: bool(abs(m.dist(end1) - m.dist(end2)) < 10**-9) + True + + Complete geodesics have no midpoint:: + + sage: HyperbolicPlane().UHP().get_geodesic(0,2).midpoint() + Traceback (most recent call last): + ... + ValueError: the length must be finite + """ + UHP = self._model.realization_of().a_realization() + P = self.to_model(UHP).midpoint() + return self._model(P) + + def dist(self, other): + r""" + Return the hyperbolic distance from a given hyperbolic geodesic + to another geodesic or point. + + INPUT: + + - ``other`` -- a hyperbolic geodesic or hyperbolic point in + the same model + + OUTPUT: + + - the hyperbolic distance + + EXAMPLES:: + + sage: g = HyperbolicPlane().UHP().get_geodesic(2, 4.0) + sage: h = HyperbolicPlane().UHP().get_geodesic(5, 7.0) + sage: bool(abs(g.dist(h).n() - 1.92484730023841) < 10**-9) + True + + If the second object is a geodesic ultraparallel to the first, + or if it is a point on the boundary that is not one of the + first object's endpoints, then return +infinity:: + + sage: g = HyperbolicPlane().UHP().get_geodesic(2, 2+I) + sage: p = HyperbolicPlane().UHP().get_point(5) + sage: g.dist(p) + +Infinity + """ + return self._model.dist(self, other) + + def angle(self, other): + r""" + Return the angle between any two given geodesics if they + intersect. + + INPUT: + + - ``other`` -- a hyperbolic geodesic in the same model as ``self`` + + OUTPUT: + + - the angle in radians between the two given geodesics + + EXAMPLES:: + + sage: PD = HyperbolicPlane().PD() + sage: g = PD.get_geodesic(3/5*I + 4/5, 15/17*I + 8/17) + sage: h = PD.get_geodesic(4/5*I + 3/5, 9/13*I + 6/13) + sage: g.angle(h) + 1/2*pi + """ + if self.is_parallel(other): + raise ValueError("geodesics do not intersect") + return self._cached_geodesic.angle(other) + + def length(self): + r""" + Return the Hyperbolic length of the hyperbolic line segment. + + EXAMPLES:: + + sage: g = HyperbolicPlane().UHP().get_geodesic(2 + I, 3 + I/2) + sage: g.length() + arccosh(9/4) + """ + return self._model._dist_points(self._start.coordinates(), self._end.coordinates()) + +##################################################################### +## UHP geodesics + +class HyperbolicGeodesicUHP(HyperbolicGeodesic): + r""" + Create a geodesic in the upper half plane model. + + INPUT: + + - ``start`` -- a :class:`HyperbolicPoint` in hyperbolic space + representing the start of the geodesic + + - ``end`` -- a :class:`HyperbolicPoint` in hyperbolic space + representing the end of the geodesic + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: g = UHP.get_geodesic(UHP.get_point(I), UHP.get_point(2 + I)) + sage: g = UHP.get_geodesic(I, 2 + I) + """ + def reflection_involution(self): + r""" + Return the isometry of the involution fixing the geodesic ``self``. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: g1 = UHP.get_geodesic(0, 1) + sage: g1.reflection_involution() + Isometry in UHP + [ 1 0] + [ 2 -1] + sage: UHP.get_geodesic(I, 2*I).reflection_involution() + Isometry in UHP + [ 1 0] + [ 0 -1] + """ + x, y = [real(k.coordinates()) for k in self.ideal_endpoints()] + if x == infinity: + M = matrix([[1,-2*y],[0,-1]]) + elif y == infinity: + M = matrix([[1,-2*x],[0,-1]]) + else: + M = matrix([[(x+y)/(y-x),- 2*x*y/(y-x)], [2/(y-x), -(x+y)/(y-x)]]) + return self._model.get_isometry(M) + + def show(self, boundary=True, **options): + r""" + Plot ``self``. + + EXAMPLES:: + + sage: HyperbolicPlane().UHP().get_geodesic(0, 1).show() + Graphics object consisting of 2 graphics primitives + """ + opts = {'axes': False, 'aspect_ratio': 1} + opts.update(self.graphics_options()) + opts.update(options) + end_1, end_2 = [CC(k.coordinates()) for k in self.endpoints()] + bd_1, bd_2 = [CC(k.coordinates()) for k in self.ideal_endpoints()] + if (abs(real(end_1) - real(end_2)) < EPSILON) \ + or CC(infinity) in [end_1, end_2]: #on same vertical line + # If one of the endpoints is infinity, we replace it with a + # large finite point + if end_1 == CC(infinity): + end_1 = (real(end_2), (imag(end_2) + 10)) + end_2 = (real(end_2), imag(end_2)) + elif end_2 == CC(infinity): + end_2 = (real(end_1), (imag(end_1) + 10)) + end_1 = (real(end_1), imag(end_1)) + from sage.plot.line import line + pic = line((end_1, end_2), **opts) + if boundary: + cent = min(bd_1, bd_2) + bd_dict = {'bd_min': cent - 3, 'bd_max': cent + 3} + bd_pic = self._model.get_background_graphic(**bd_dict) + pic = bd_pic + pic + return pic + else: + center = (bd_1 + bd_2)/2 # Circle center + radius = abs(bd_1 - bd_2)/2 + theta1 = CC(end_1 - center).arg() + theta2 = CC(end_2 - center).arg() + if abs(theta1 - theta2) < EPSILON: + theta2 += pi + [theta1, theta2] = sorted([theta1, theta2]) + from sage.calculus.var import var + from sage.plot.plot import parametric_plot + x = var('x') + pic = parametric_plot((radius*cos(x) + real(center), + radius*sin(x) + imag(center)), + (x, theta1, theta2), **opts) + if boundary: + # We want to draw a segment of the real line. The + # computations below compute the projection of the + # geodesic to the real line, and then draw a little + # to the left and right of the projection. + shadow_1, shadow_2 = [real(k) for k in [end_1, end_2]] + midpoint = (shadow_1 + shadow_2)/2 + length = abs(shadow_1 - shadow_2) + bd_dict = {'bd_min': midpoint - length, 'bd_max': midpoint + + length} + bd_pic = self._model.get_background_graphic(**bd_dict) + pic = bd_pic + pic + return pic + + def ideal_endpoints(self): + r""" + Determine the ideal (boundary) endpoints of the complete + hyperbolic geodesic corresponding to ``self``. + + OUTPUT: + + - a list of 2 boundary points + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: UHP.get_geodesic(I, 2*I).ideal_endpoints() + [Boundary point in UHP 0, + Boundary point in UHP +Infinity] + sage: UHP.get_geodesic(1 + I, 2 + 4*I).ideal_endpoints() + [Boundary point in UHP -sqrt(65) + 9, + Boundary point in UHP sqrt(65) + 9] + """ + start = self._start.coordinates() + end = self._end.coordinates() + [x1, x2] = [real(k) for k in [start, end]] + [y1, y2] = [imag(k) for k in [start, end]] + M = self._model + # infinity is the first endpoint, so the other ideal endpoint + # is just the real part of the second coordinate + if start == infinity: + return [M.get_point(start), M.get_point(x2)] + # Same idea as above + if end == infinity: + return [M.get_point(x1), M.get_point(end)] + # We could also have a vertical line with two interior points + if x1 == x2: + return [M.get_point(x1), M.get_point(infinity)] + # Otherwise, we have a semicircular arc in the UHP + c = ((x1+x2)*(x2-x1) + (y1+y2)*(y2-y1)) / (2*(x2-x1)) + r = sqrt((c - x1)**2 + y1**2) + return [M.get_point(c - r), M.get_point(c + r)] + + def common_perpendicular(self, other): + r""" + Return the unique hyperbolic geodesic perpendicular to ``self`` + and ``other``, if such a geodesic exists; otherwise raise a + ``ValueError``. + + INPUT: + + - ``other`` -- a hyperbolic geodesic in current model + + OUTPUT: + + - a hyperbolic geodesic + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: g = UHP.get_geodesic(2, 3) + sage: h = UHP.get_geodesic(4, 5) + sage: g.common_perpendicular(h) + Geodesic in UHP from 1/2*sqrt(3) + 7/2 to -1/2*sqrt(3) + 7/2 + + It is an error to ask for the common perpendicular of two + intersecting geodesics:: + + sage: g = UHP.get_geodesic(2, 4) + sage: h = UHP.get_geodesic(3, infinity) + sage: g.common_perpendicular(h) + Traceback (most recent call last): + ... + ValueError: geodesics intersect; no common perpendicular exists + """ + # Make sure both are in the same model + if other._model is not self._model: + other = other.to_model(self._model) + + A = self.reflection_involution() + B = other.reflection_involution() + C = A * B + if C.classification() != 'hyperbolic': + raise ValueError("geodesics intersect; no common perpendicular exists") + return C.fixed_point_set() + + def intersection(self, other): + r""" + Return the point of intersection of ``self`` and ``other`` + (if such a point exists). + + INPUT: + + - ``other`` -- a hyperbolic geodesic in the current model + + OUTPUT: + + - a list of hyperbolic points or a hyperbolic geodesic + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: g = UHP.get_geodesic(3, 5) + sage: h = UHP.get_geodesic(4, 7) + sage: g.intersection(h) + [Point in UHP 2/3*sqrt(-2) + 13/3] + + If the given geodesics do not intersect, the function returns an + empty list:: + + sage: g = UHP.get_geodesic(4, 5) + sage: h = UHP.get_geodesic(5, 7) + sage: g.intersection(h) + [] + + If the given geodesics are identical, return that + geodesic:: + + sage: g = UHP.get_geodesic(4 + I, 18*I) + sage: h = UHP.get_geodesic(4 + I, 18*I) + sage: g.intersection(h) + [Boundary point in UHP -1/8*sqrt(114985) - 307/8, + Boundary point in UHP 1/8*sqrt(114985) - 307/8] + """ + start_1, end_1 = sorted(self.ideal_endpoints(), key=str) + start_2, end_2 = sorted(other.ideal_endpoints(), key=str) + if start_1 == start_2 and end_1 == end_2: # Unoriented geods are same + return [start_1, end_1] + if start_1 == start_2: + return start_1 + elif end_1 == end_2: + return end_1 + A = self.reflection_involution() + B = other.reflection_involution() + C = A * B + if C.classification() in ['hyperbolic', 'parabolic']: + return [] + return C.fixed_point_set() + + def perpendicular_bisector(self): #UHP + r""" + Return the perpendicular bisector of the hyperbolic geodesic ``self`` + if that geodesic has finite length. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: g = UHP.random_geodesic() + sage: h = g.perpendicular_bisector() + sage: c = lambda x: x.coordinates() + sage: bool(c(g.intersection(h)[0]) - c(g.midpoint()) < 10**-9) + True + + Infinite geodesics cannot be bisected:: + + sage: UHP.get_geodesic(0, 1).perpendicular_bisector() + Traceback (most recent call last): + ... + ValueError: the length must be finite + """ + if self.length() == infinity: + raise ValueError("the length must be finite") + start = self._start.coordinates() + d = self._model._dist_points(start, self._end.coordinates()) / 2 + S = self.complete()._to_std_geod(start) + T1 = matrix([[exp(d/2), 0], [0, exp(-d/2)]]) + s2 = sqrt(2) * 0.5 + T2 = matrix([[s2, -s2], [s2, s2]]) + isom_mtrx = S.inverse() * (T1 * T2) * S # We need to clean this matrix up. + if (isom_mtrx - isom_mtrx.conjugate()).norm() < 5*EPSILON: # Imaginary part is small. + isom_mtrx = (isom_mtrx + isom_mtrx.conjugate()) / 2 # Set it to its real part. + H = self._model.get_isometry(isom_mtrx) + return self._model.get_geodesic(H(self._start), H(self._end)) + + def midpoint(self): # UHP + r""" + Return the (hyperbolic) midpoint of ``self`` if it exists. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: g = UHP.random_geodesic() + sage: m = g.midpoint() + sage: d1 = UHP.dist(m, g.start()) + sage: d2 = UHP.dist(m, g.end()) + sage: bool(abs(d1 - d2) < 10**-9) + True + + Infinite geodesics have no midpoint:: + + sage: UHP.get_geodesic(0, 2).midpoint() + Traceback (most recent call last): + ... + ValueError: the length must be finite + """ + if self.length() == infinity: + raise ValueError("the length must be finite") + + start = self._start.coordinates() + end = self._end.coordinates() + d = self._model._dist_points(start, end) / 2 + S = self.complete()._to_std_geod(start) + T = matrix([[exp(d), 0], [0, 1]]) + M = S.inverse() * T * S + if ((real(start - end) < EPSILON) + or (abs(real(start - end)) < EPSILON + and imag(start - end) < EPSILON)): + end_p = start + else: + end_p = end + return self._model.get_point(mobius_transform(M, end_p)) + + def angle(self, other): #UHP + r""" + Return the angle between any two given completed geodesics if + they intersect. + + INPUT: + + - ``other`` -- a hyperbolic geodesic in the UHP model + + OUTPUT: + + - the angle in radians between the two given geodesics + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: g = UHP.get_geodesic(2, 4) + sage: h = UHP.get_geodesic(3, 3 + I) + sage: g.angle(h) + 1/2*pi + sage: numerical_approx(g.angle(h)) + 1.57079632679490 + + If the geodesics are identical, return angle 0:: + + sage: g.angle(g) + 0 + + It is an error to ask for the angle of two geodesics that do not + intersect:: + + sage: g = UHP.get_geodesic(2, 4) + sage: h = UHP.get_geodesic(5, 7) + sage: g.angle(h) + Traceback (most recent call last): + ... + ValueError: geodesics do not intersect + """ + if self.is_parallel(other): + raise ValueError("geodesics do not intersect") + # Make sure the segments are complete or intersect + if (not(self.is_complete() and other.is_complete()) + and not self.intersection(other)): + print("Warning: Geodesic segments do not intersect. " + "The angle between them is not defined.\n" + "Returning the angle between their completions.") + + # Make sure both are in the same model + if other._model is not self._model: + other = other.to_model(self._model) + + (p1, p2) = sorted([k.coordinates() + for k in self.ideal_endpoints()], key=str) + (q1, q2) = sorted([k.coordinates() + for k in other.ideal_endpoints()], key=str) + # if the geodesics are equal, the angle between them is 0 + if (abs(p1 - q1) < EPSILON + and abs(p2 - q2) < EPSILON): + return 0 + elif p2 != infinity: # geodesic not a straight line + # So we send it to the geodesic with endpoints [0, oo] + T = HyperbolicGeodesicUHP._crossratio_matrix(p1, (p1+p2)/2, p2) + else: + # geodesic is a straight line, so we send it to the geodesic + # with endpoints [0,oo] + T = HyperbolicGeodesicUHP._crossratio_matrix(p1, p1+1, p2) + # b1 and b2 are the endpoints of the image of other + b1, b2 = [mobius_transform(T, k) for k in [q1, q2]] + # If other is now a straight line... + if (b1 == infinity or b2 == infinity): + # then since they intersect, they are equal + return 0 + return real(arccos((b1 + b2) / abs(b2 - b1))) + + ################## + # Helper methods # + ################## + + def _to_std_geod(self, p): + r""" + Given the coordinates of a geodesic in hyperbolic space, return the + hyperbolic isometry that sends that geodesic to the geodesic + through 0 and infinity that also sends the point ``p`` to `i`. + + INPUT: + + - ``p`` -- the coordinates of the + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: (p1, p2) = [UHP.random_point() for k in range(2)] + sage: g = UHP.get_geodesic(p1, p2) + sage: A = g._to_std_geod(g.midpoint().coordinates()) # Send midpoint to I. + sage: A = UHP.get_isometry(A) + sage: [s, e]= g.complete().endpoints() + sage: bool(abs(A(s).coordinates()) < 10**-9) + True + sage: bool(abs(A(g.midpoint()).coordinates() - I) < 10**-9) + True + sage: bool(A(e).coordinates() == infinity) + True + """ + B = matrix([[1, 0], [0, -I]]) + [s, e] = [k.coordinates() for k in self.complete().endpoints()] + # outmat below will be returned after we normalize the determinant. + outmat = B * HyperbolicGeodesicUHP._crossratio_matrix(s, p, e) + outmat = outmat / outmat.det().sqrt() + if (outmat - outmat.conjugate()).norm(1) < 10**-9: # Small imaginary part. + outmat = (outmat + outmat.conjugate()) / 2 # Set it equal to its real part. + return outmat + + @staticmethod + def _crossratio_matrix(p0, p1, p2): #UHP + r""" + Given three points (the list `p`) in `\mathbb{CP}^{1}` in affine + coordinates, return the linear fractional transformation taking + the elements of `p` to `0`,`1`, and `\infty'. + + INPUT: + + - a list of three distinct elements of three distinct elements + of `\mathbb{CP}^1` in affine coordinates; that is, each element + must be a complex number, `\infty`, or symbolic. + + OUTPUT: + + - an element of `\GL(2,\CC)` + + EXAMPLES:: + + sage: from sage.geometry.hyperbolic_space.hyperbolic_geodesic import HyperbolicGeodesicUHP + sage: from sage.geometry.hyperbolic_space.hyperbolic_isometry import mobius_transform + sage: UHP = HyperbolicPlane().UHP() + sage: (p1, p2, p3) = [UHP.random_point().coordinates() for k in range(3)] + sage: A = HyperbolicGeodesicUHP._crossratio_matrix(p1, p2, p3) + sage: bool(abs(mobius_transform(A, p1)) < 10**-9) + True + sage: bool(abs(mobius_transform(A, p2) - 1) < 10**-9) + True + sage: bool(mobius_transform(A, p3) == infinity) + True + sage: (x,y,z) = var('x,y,z'); HyperbolicGeodesicUHP._crossratio_matrix(x,y,z) + [ y - z -x*(y - z)] + [ -x + y (x - y)*z] + """ + if p0 == infinity: + return matrix([[0, -(p1 - p2)], [-1, p2]]) + elif p1 == infinity: + return matrix([[1, -p0], [1, -p2]]) + elif p2 == infinity: + return matrix([[1, -p0], [0, p1 - p0]]) + return matrix([[p1 - p2, (p1 - p2)*(-p0)], + [p1 - p0, (p1 - p0)*(-p2)]]) + +##################################################################### +## Other geodesics + +class HyperbolicGeodesicPD(HyperbolicGeodesic): + r""" + A geodesic in the Poincaré disk model. + + INPUT: + + - ``start`` -- a :class:`HyperbolicPoint` in hyperbolic space + representing the start of the geodesic + + - ``end`` -- a :class:`HyperbolicPoint` in hyperbolic space + representing the end of the geodesic + + EXAMPLES:: + + sage: PD = HyperbolicPlane().PD() + sage: g = PD.get_geodesic(PD.get_point(I), PD.get_point(I/2)) + sage: g = PD.get_geodesic(I, I/2) + """ + def show(self, boundary=True, **options): + r""" + Plot ``self``. + + EXAMPLES:: + + sage: HyperbolicPlane().PD().get_geodesic(0, 1).show() + Graphics object consisting of 2 graphics primitives + """ + opts = dict([('axes', False), ('aspect_ratio', 1)]) + opts.update(self.graphics_options()) + opts.update(options) + end_1, end_2 = [CC(k.coordinates()) for k in self.endpoints()] + bd_1, bd_2 = [CC(k.coordinates()) for k in self.ideal_endpoints()] + # Check to see if it's a line + if bool(real(bd_1)*imag(bd_2) - real(bd_2)*imag(bd_1))**2 < EPSILON: + from sage.plot.line import line + pic = line([(real(bd_1),imag(bd_1)),(real(bd_2),imag(bd_2))], + **opts) + else: + # If we are here, we know it's not a line + # So we compute the center and radius of the circle + center = (1/(real(bd_1)*imag(bd_2) - real(bd_2)*imag(bd_1)) * + ((imag(bd_2)-imag(bd_1)) + (real(bd_1)-real(bd_2))*I)) + radius = RR(abs(bd_1 - center)) # abs is Euclidean distance + # Now we calculate the angles for the parametric plot + theta1 = CC(end_1 - center).arg() + theta2 = CC(end_2 - center).arg() + if theta2 < theta1: + theta1, theta2 = theta2, theta1 + from sage.calculus.var import var + from sage.plot.plot import parametric_plot + x = var('x') + mid = (theta1 + theta2)/2.0 + if (radius*cos(mid) + real(center))**2 + \ + (radius*sin(mid) + imag(center))**2 > 1.0: + # Swap theta1 and theta2 + tmp = theta1 + 2*pi + theta1 = theta2 + theta2 = tmp + pic = parametric_plot((radius*cos(x) + real(center), + radius*sin(x) + imag(center)), + (x, theta1, theta2), **opts) + + else: + pic = parametric_plot((radius*cos(x) + real(center), + radius*sin(x) + imag(center)), + (x, theta1, theta2), **opts) + if boundary: + bd_pic = self._model.get_background_graphic() + pic = bd_pic + pic + return pic + + +class HyperbolicGeodesicKM(HyperbolicGeodesic): + r""" + A geodesic in the Klein disk model. + + INPUT: + + - ``start`` -- a :class:`HyperbolicPoint` in hyperbolic space + representing the start of the geodesic + + - ``end`` -- a :class:`HyperbolicPoint` in hyperbolic space + representing the end of the geodesic + + EXAMPLES:: + + sage: KM = HyperbolicPlane().KM() + sage: g = KM.get_geodesic(KM.get_point((0,1)), KM.get_point((0,1/2))) + sage: g = KM.get_geodesic((0,1), (0,1/2)) + """ + def show(self, boundary=True, **options): + r""" + Plot ``self``. + + EXAMPLES:: + + sage: HyperbolicPlane().KM().get_geodesic((0,0), (1,0)).show() + Graphics object consisting of 2 graphics primitives + """ + from sage.plot.line import line + opts = {'axes': False, 'aspect_ratio': 1} + opts.update(self.graphics_options()) + pic = line([k.coordinates() for k in self.endpoints()], **opts) + if boundary: + bd_pic = self._model.get_background_graphic() + pic = bd_pic + pic + return pic + + +class HyperbolicGeodesicHM(HyperbolicGeodesic): + r""" + A geodesic in the hyperboloid model. + + INPUT: + + - ``start`` -- a :class:`HyperbolicPoint` in hyperbolic space + representing the start of the geodesic + + - ``end`` -- a :class:`HyperbolicPoint` in hyperbolic space + representing the end of the geodesic + + EXAMPLES:: + + sage: from sage.geometry.hyperbolic_space.hyperbolic_geodesic import * + sage: HM = HyperbolicPlane().HM() + sage: g = HM.get_geodesic(HM.get_point((0, 0, 1)), HM.get_point((0,1,sqrt(2)))) + sage: g = HM.get_geodesic((0, 0, 1), (0, 1, sqrt(2))) + """ + def show(self, show_hyperboloid=True, **graphics_options): + r""" + Plot ``self``. + + EXAMPLES:: + + sage: from sage.geometry.hyperbolic_space.hyperbolic_geodesic import * + sage: g = HyperbolicPlane().HM().random_geodesic() + sage: g.show() + Graphics3d Object + """ + from sage.calculus.var import var + (x,y,z) = var('x,y,z') + opts = self.graphics_options() + opts.update(graphics_options) + v1,u2 = [vector(k.coordinates()) for k in self.endpoints()] + # Lorentzian Gram Shmidt. The original vectors will be + # u1, u2 and the orthogonal ones will be v1, v2. Except + # v1 = u1, and I don't want to declare another variable, + # hence the odd naming convention above. + # We need the Lorentz dot product of v1 and u2. + v1_ldot_u2 = u2[0]*v1[0] + u2[1]*v1[1] - u2[2]*v1[2] + v2 = u2 + v1_ldot_u2*v1 + v2_norm = sqrt(v2[0]**2 + v2[1]**2 - v2[2]**2) + v2 = v2/v2_norm + v2_ldot_u2 = u2[0]*v2[0] + u2[1]*v2[1] - u2[2]*v2[2] + # Now v1 and v2 are Lorentz orthogonal, and |v1| = -1, |v2|=1 + # That is, v1 is unit timelike and v2 is unit spacelike. + # This means that cosh(x)*v1 + sinh(x)*v2 is unit timelike. + hyperbola = cosh(x)*v1 + sinh(x)*v2 + endtime = arcsinh(v2_ldot_u2) + from sage.plot.plot3d.all import parametric_plot3d + pic = parametric_plot3d(hyperbola,(x,0, endtime), **graphics_options) + if show_hyperboloid: + bd_pic = self._model.get_background_graphic() + pic = bd_pic + pic + return pic diff --git a/src/sage/geometry/hyperbolic_space/hyperbolic_interface.py b/src/sage/geometry/hyperbolic_space/hyperbolic_interface.py new file mode 100644 index 00000000000..d8d3c411467 --- /dev/null +++ b/src/sage/geometry/hyperbolic_space/hyperbolic_interface.py @@ -0,0 +1,234 @@ +# -*- coding: utf-8 -*- +r""" +Interface to Hyperbolic Models + +This module provides a convenient interface for interacting with models +of hyperbolic space as well as their points, geodesics, and isometries. + +The primary point of this module is to allow the code that implements +hyperbolic space to be sufficiently decoupled while still providing a +convenient user experience. + +The interfaces are by default given abbreviated names. For example, +UHP (upper half plane model), PD (Poincaré disk model), KM (Klein disk +model), and HM (hyperboloid model). + +.. NOTE:: + + All of the current models of 2 dimensional hyperbolic space + use the upper half plane model for their computations. This can + lead to some problems, such as long coordinate strings for symbolic + points. For example, the vector ``(1, 0, sqrt(2))`` defines a point + in the hyperboloid model. Performing mapping this point to the upper + half plane and performing computations there may return with vector + whose components are unsimplified strings have several ``sqrt(2)``'s. + Presently, this drawback is outweighed by the rapidity with which new + models can be implemented. + +AUTHORS: + +- Greg Laun (2013): Initial version. +- Rania Amer, Jean-Philippe Burelle, Bill Goldman, Zach Groton, + Jeremy Lent, Leila Vaden, Derrick Wigglesworth (2011): many of the + methods spread across the files. + +EXAMPLES:: + + sage: HyperbolicPlane().UHP().get_point(2 + I) + Point in UHP I + 2 + + sage: HyperbolicPlane().PD().get_point(1/2 + I/2) + Point in PD 1/2*I + 1/2 +""" + +#*********************************************************************** +# +# Copyright (C) 2013 Greg Laun +# +# +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# http://www.gnu.org/licenses/ +#*********************************************************************** +from sage.structure.unique_representation import UniqueRepresentation +from sage.structure.parent import Parent +from sage.misc.abstract_method import abstract_method +from sage.categories.sets_cat import Sets +from sage.categories.realizations import Realizations, Category_realization_of_parent +from sage.geometry.hyperbolic_space.hyperbolic_model import ( + HyperbolicModelUHP, HyperbolicModelPD, + HyperbolicModelHM, HyperbolicModelKM) + + +def HyperbolicSpace(n): + """ + Return ``n`` dimensional hyperbolic space. + + EXAMPLES:: + + sage: from sage.geometry.hyperbolic_space.hyperbolic_interface import HyperbolicSpace + sage: HyperbolicSpace(2) + Hyperbolic plane + """ + if n == 2: + return HyperbolicPlane() + raise NotImplementedError("currently only implemented in dimension 2") + + +class HyperbolicPlane(Parent, UniqueRepresentation): + """ + The hyperbolic plane `\mathbb{H}^2`. + + Here are the models currently implemented: + + - ``UHP`` -- upper half plane + - ``PD`` -- Poincaré disk + - ``KM`` -- Klein disk + - ``HM`` -- hyperboloid model + """ + def __init__(self): + """ + Initialize ``self``. + + EXAMPLES:: + + sage: H = HyperbolicPlane() + sage: TestSuite(H).run() + """ + Parent.__init__(self, category=Sets().WithRealizations()) + self.a_realization() # We create a realization so at least one is known + + def _repr_(self): + """ + Return a string representation of ``self``. + + EXAMPLES:: + + sage: HyperbolicPlane() + Hyperbolic plane + """ + return "Hyperbolic plane" + + def a_realization(self): + """ + Return a realization of ``self``. + + EXAMPLES:: + + sage: H = HyperbolicPlane() + sage: H.a_realization() + Hyperbolic plane in the Upper Half Plane Model model + """ + return self.UHP() + + UHP = HyperbolicModelUHP + UpperHalfPlane = UHP + + PD = HyperbolicModelPD + PoincareDisk = PD + + KM = HyperbolicModelKM + KleinDisk = KM + + HM = HyperbolicModelHM + Hyperboloid = HM + + +class HyperbolicModels(Category_realization_of_parent): + r""" + The category of hyperbolic models of hyperbolic space. + """ + def __init__(self, base): + r""" + Initialize the hyperbolic models of hyperbolic space. + + INPUT: + + - ``base`` -- a hyperbolic space + + TESTS:: + + sage: from sage.geometry.hyperbolic_space.hyperbolic_interface import HyperbolicModels + sage: H = HyperbolicPlane() + sage: models = HyperbolicModels(H) + sage: H.UHP() in models + True + """ + Category_realization_of_parent.__init__(self, base) + + def _repr_(self): + r""" + Return the representation of ``self``. + + EXAMPLES:: + + sage: from sage.geometry.hyperbolic_space.hyperbolic_interface import HyperbolicModels + sage: H = HyperbolicPlane() + sage: HyperbolicModels(H) + Category of hyperbolic models of Hyperbolic plane + """ + return "Category of hyperbolic models of {}".format(self.base()) + + def super_categories(self): + r""" + The super categories of ``self``. + + EXAMPLES:: + + sage: from sage.geometry.hyperbolic_space.hyperbolic_interface import HyperbolicModels + sage: H = HyperbolicPlane() + sage: models = HyperbolicModels(H) + sage: models.super_categories() + [Category of sets, Category of realizations of Hyperbolic plane] + """ + return [Sets(), Realizations(self.base())] + + class ParentMethods: + def _an_element_(self): + """ + Return an element of ``self``. + + EXAMPLES:: + + sage: H = HyperbolicPlane() + sage: H.UHP().an_element() + Point in UHP I + sage: H.PD().an_element() + Point in PD 0 + sage: H.KM().an_element() + Point in KM (0, 0) + sage: H.HM().an_element() + Point in HM (0, 0, 1) + """ + return self(self.realization_of().PD().get_point(0)) + + # TODO: Move to a category of metric spaces once created + @abstract_method + def dist(self, a, b): + """ + Return the distance between ``a`` and ``b``. + + EXAMPLES:: + + sage: PD = HyperbolicPlane().PD() + sage: PD.dist(PD.get_point(0), PD.get_point(I/2)) + arccosh(5/3) + """ + + class ElementMethods: + # TODO: Move to a category of metric spaces once created + def dist(self, other): + """ + Return the distance between ``self`` and ``other``. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: p1 = UHP.get_point(5 + 7*I) + sage: p2 = UHP.get_point(1 + I) + sage: p1.dist(p2) + arccosh(33/7) + """ + return self.parent().dist(self, other) diff --git a/src/sage/geometry/hyperbolic_space/hyperbolic_isometry.py b/src/sage/geometry/hyperbolic_space/hyperbolic_isometry.py new file mode 100644 index 00000000000..4840c055eaf --- /dev/null +++ b/src/sage/geometry/hyperbolic_space/hyperbolic_isometry.py @@ -0,0 +1,1082 @@ +r""" +Hyperbolic Isometries + +This module implements the abstract base class for isometries of +hyperbolic space of arbitrary dimension. It also contains the +implementations for specific models of hyperbolic geometry. + +The isometry groups of all implemented models are either matrix Lie +groups or are doubly covered by matrix Lie groups. As such, the +isometry constructor takes a matrix as input. However, since the +isometries themselves may not be matrices, quantities like the trace +and determinant are not directly accessible from this class. + +AUTHORS: + +- Greg Laun (2013): initial version + +EXAMPLES: + +We can construct isometries in the upper half plane model, abbreviated +UHP for convenience:: + + sage: UHP = HyperbolicPlane().UHP() + sage: UHP.get_isometry(matrix(2,[1,2,3,4])) + Isometry in UHP + [1 2] + [3 4] + sage: A = UHP.get_isometry(matrix(2,[0,1,1,0])) + sage: A.inverse() + Isometry in UHP + [0 1] + [1 0] +""" + +#*********************************************************************** +# Copyright (C) 2013 Greg Laun +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# http://www.gnu.org/licenses/ +#*********************************************************************** + +from copy import copy +from sage.categories.homset import Hom +from sage.categories.morphism import Morphism +from sage.misc.lazy_attribute import lazy_attribute +from sage.matrix.constructor import matrix +from sage.modules.free_module_element import vector +from sage.rings.infinity import infinity +from sage.misc.latex import latex +from sage.rings.all import RDF +from sage.functions.other import imag, sqrt +from sage.functions.all import arccosh, sign + +from sage.geometry.hyperbolic_space.hyperbolic_constants import EPSILON +from sage.geometry.hyperbolic_space.hyperbolic_geodesic import HyperbolicGeodesic + +class HyperbolicIsometry(Morphism): + r""" + Abstract base class for hyperbolic isometries. This class should + never be instantiated. + + INPUT: + + - ``A`` -- a matrix representing a hyperbolic isometry in the + appropriate model + + EXAMPLES:: + + sage: HyperbolicPlane().HM().get_isometry(identity_matrix(3)) + Isometry in HM + [1 0 0] + [0 1 0] + [0 0 1] + """ + + ##################### + # "Private" Methods # + ##################### + + def __init__(self, model, A, check=True): + r""" + See :class:`HyperbolicIsometry` for full documentation. + + EXAMPLES:: + + sage: A = HyperbolicPlane().UHP().get_isometry(matrix(2, [0,1,-1,0])) + sage: TestSuite(A).run(skip="_test_category") + """ + if check: + model.isometry_test(A) + self._matrix = copy(A) # Make a copy of the potentially mutable matrix + self._matrix.set_immutable() # Make it immutable + Morphism.__init__(self, Hom(model, model)) + + @lazy_attribute + def _cached_isometry(self): + r""" + The representation of the current isometry used for + calculations. For example, if the current model uses the + upper half plane, then ``_cached_isometry`` will + hold the `SL(2,\RR)` representation of ``self.matrix()``. + + EXAMPLES:: + + sage: A = HyperbolicPlane().HM().get_isometry(identity_matrix(3)) + sage: A._cached_isometry + Isometry in UHP + [1 0] + [0 1] + """ + R = self.domain().realization_of().a_realization() + return self.to_model(R) + + def _repr_(self): + r""" + Return a string representation of ``self``. + + OUTPUT: + + - a string + + EXAMPLES:: + + sage: HyperbolicPlane().UHP().get_isometry(identity_matrix(2)) + Isometry in UHP + [1 0] + [0 1] + """ + return self._repr_type() + " in {0}\n{1}".format(self.domain().short_name(), self._matrix) + + def _repr_type(self): + r""" + Return the type of morphism. + + EXAMPLES:: + + sage: A = HyperbolicPlane().UHP().get_isometry(identity_matrix(2)) + sage: A._repr_type() + 'Isometry' + """ + return "Isometry" + + def _latex_(self): + r""" + EXAMPLES:: + + sage: A = HyperbolicPlane().UHP().get_isometry(identity_matrix(2)) + sage: latex(A) + \pm \left(\begin{array}{rr} + 1 & 0 \\ + 0 & 1 + \end{array}\right) + + sage: B = HyperbolicPlane().HM().get_isometry(identity_matrix(3)) + sage: latex(B) + \left(\begin{array}{rrr} + 1 & 0 & 0 \\ + 0 & 1 & 0 \\ + 0 & 0 & 1 + \end{array}\right) + """ + if self.domain().is_isometry_group_projective(): + return "\pm " + latex(self._matrix) + else: + return latex(self._matrix) + + def __eq__(self, other): + r""" + Return ``True`` if the isometries are the same and ``False`` otherwise. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: A = UHP.get_isometry(identity_matrix(2)) + sage: B = UHP.get_isometry(-identity_matrix(2)) + sage: A == B + True + + sage: HM = HyperbolicPlane().HM() + sage: A = HM.random_isometry() + sage: A == A + True + """ + if not isinstance(other, HyperbolicIsometry): + return False + test_matrix = bool((self.matrix() - other.matrix()).norm() < EPSILON) + if self.domain().is_isometry_group_projective(): + A,B = self.matrix(), other.matrix() # Rename for simplicity + m = self.matrix().ncols() + A = A / sqrt(A.det(), m) # Normalized to have determinant 1 + B = B / sqrt(B.det(), m) + test_matrix = ((A - B).norm() < EPSILON + or (A + B).norm() < EPSILON) + return self.domain() is other.domain() and test_matrix + + def __hash__(self): + """ + Return the hash of ``self``. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: A = UHP.get_isometry(identity_matrix(2)) + sage: B = UHP.get_isometry(-identity_matrix(2)) + sage: hash(A) == hash(B) + True + + sage: HM = HyperbolicPlane().HM() + sage: A = HM.random_isometry() + sage: hash(A) == hash(A) + True + """ + if self.domain().is_isometry_group_projective(): + # Special care must be taken for projective groups + m = matrix(self._matrix.nrows(), map(abs, self._matrix.list())) + m.set_immutable() + else: + m = self._matrix + return hash((self.domain(), self.codomain(), m)) + + def __pow__(self, n): + r""" + EXAMPLES:: + + sage: A = HyperbolicPlane().UHP().get_isometry(matrix(2,[3,1,2,1])) + sage: A**3 + Isometry in UHP + [41 15] + [30 11] + """ + return self.__class__(self.domain(), self._matrix**n) + + def __mul__(self, other): + r""" + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: A = UHP.get_isometry(Matrix(2,[5,2,1,2])) + sage: B = UHP.get_isometry(Matrix(2,[3,1,1,2])) + sage: B * A + Isometry in UHP + [16 8] + [ 7 6] + sage: A = UHP.get_isometry(Matrix(2,[5,2,1,2])) + sage: p = UHP.get_point(2 + I) + sage: A * p + Point in UHP 8/17*I + 53/17 + + sage: g = UHP.get_geodesic(2 + I, 4 + I) + sage: A * g + Geodesic in UHP from 8/17*I + 53/17 to 8/37*I + 137/37 + + sage: A = diagonal_matrix([1, -1, 1]) + sage: A = HyperbolicPlane().HM().get_isometry(A) + sage: A.preserves_orientation() + False + sage: p = HyperbolicPlane().HM().get_point((0, 1, sqrt(2))) + sage: A * p + Point in HM (0, -1, sqrt(2)) + """ + if isinstance(other, HyperbolicIsometry): + other = other.to_model(self.codomain()) + return self.__class__(self.codomain(), self._matrix*other._matrix) + from sage.geometry.hyperbolic_space.hyperbolic_point import HyperbolicPoint + if isinstance(other, HyperbolicPoint): + return self(other) + if isinstance(other, HyperbolicGeodesic): + return self.codomain().get_geodesic(self(other.start()), self(other.end())) + + raise NotImplementedError("multiplication is not defined between a " + "hyperbolic isometry and {0}".format(other)) + + def _call_(self, p): + r""" + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: A = UHP.get_isometry(Matrix(2,[5,2,1,2])) + sage: p = UHP.get_point(2 + I) + sage: A(p) + Point in UHP 8/17*I + 53/17 + + sage: A = diagonal_matrix([1, -1, 1]) + sage: A = HyperbolicPlane().HM().get_isometry(A) + sage: A.preserves_orientation() + False + sage: p = HyperbolicPlane().HM().get_point((0, 1, sqrt(2))) + sage: A(p) + Point in HM (0, -1, sqrt(2)) + + sage: I2 = UHP.get_isometry(identity_matrix(2)) + sage: p = UHP.random_point() + sage: bool(UHP.dist(I2(p), p) < 10**-9) + True + """ + return self.codomain().get_point(self._matrix * vector(p._coordinates)) + + ####################### + # Setters and Getters # + ####################### + + def matrix(self): + r""" + Return the matrix of the isometry. + + .. NOTE:: + + We do not allow the ``matrix`` constructor to work as these may + be elements of a projective group (ex. `PSL(n, \RR)`), so these + isometries aren't true matrices. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: UHP.get_isometry(-identity_matrix(2)).matrix() + [-1 0] + [ 0 -1] + """ + return self._matrix + + def inverse(self): + r""" + Return the inverse of the isometry ``self``. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: A = UHP.get_isometry(matrix(2,[4,1,3,2])) + sage: B = A.inverse() + sage: A*B == UHP.get_isometry(identity_matrix(2)) + True + """ + return self.__class__(self.domain(), self.matrix().inverse()) + + __invert__ = inverse + + def is_identity(self): + """ + Return ``True`` if ``self`` is the identity isometry. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: UHP.get_isometry(matrix(2,[4,1,3,2])).is_identity() + False + sage: UHP.get_isometry(identity_matrix(2)).is_identity() + True + """ + return self._matrix.is_one() + + def model(self): + r""" + Return the model to which ``self`` belongs. + + EXAMPLES:: + + sage: HyperbolicPlane().UHP().get_isometry(identity_matrix(2)).model() + Hyperbolic plane in the Upper Half Plane Model model + + sage: HyperbolicPlane().PD().get_isometry(identity_matrix(2)).model() + Hyperbolic plane in the Poincare Disk Model model + + sage: HyperbolicPlane().KM().get_isometry(identity_matrix(3)).model() + Hyperbolic plane in the Klein Disk Model model + + sage: HyperbolicPlane().HM().get_isometry(identity_matrix(3)).model() + Hyperbolic plane in the Hyperboloid Model model + """ + return self.domain() + + def to_model(self, other): + r""" + Convert the current object to image in another model. + + INPUT: + + - ``other`` -- (a string representing) the image model + + EXAMPLES:: + + sage: H = HyperbolicPlane() + sage: UHP = H.UHP() + sage: PD = H.PD() + sage: KM = H.KM() + sage: HM = H.HM() + + sage: A = UHP.get_isometry(identity_matrix(2)) + sage: A.to_model(HM) + Isometry in HM + [1 0 0] + [0 1 0] + [0 0 1] + sage: A.to_model('HM') + Isometry in HM + [1 0 0] + [0 1 0] + [0 0 1] + + sage: A = PD.get_isometry(matrix([[I, 0], [0, -I]])) + sage: A.to_model(UHP) + Isometry in UHP + [ 0 1] + [-1 0] + sage: A.to_model(HM) + Isometry in HM + [-1 0 0] + [ 0 -1 0] + [ 0 0 1] + sage: A.to_model(KM) + Isometry in KM + [-1 0 0] + [ 0 -1 0] + [ 0 0 1] + + sage: A = HM.get_isometry(diagonal_matrix([-1, -1, 1])) + sage: A.to_model('UHP') + Isometry in UHP + [ 0 -1] + [ 1 0] + sage: A.to_model('PD') + Isometry in PD + [-I 0] + [ 0 I] + sage: A.to_model('KM') + Isometry in KM + [-1 0 0] + [ 0 -1 0] + [ 0 0 1] + """ + if isinstance(other, str): + other = getattr(self.domain().realization_of(), other)() + if other is self.domain(): + return self + phi = other.coerce_map_from(self.domain()) + return phi.convert_isometry(self) + + ################### + # Boolean Methods # + ################### + + def preserves_orientation(self): + r""" + Return ``True`` if ``self`` is orientation preserving and ``False`` + otherwise. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: A = UHP.get_isometry(identity_matrix(2)) + sage: A.preserves_orientation() + True + sage: B = UHP.get_isometry(matrix(2,[0,1,1,0])) + sage: B.preserves_orientation() + False + """ + return self._cached_isometry.preserves_orientation() + + def classification(self): + r""" + Classify the hyperbolic isometry as elliptic, parabolic, + hyperbolic or a reflection. + + A hyperbolic isometry fixes two points on the boundary of + hyperbolic space, a parabolic isometry fixes one point on the + boundary of hyperbolic space, and an elliptic isometry fixes no + points. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: H = UHP.get_isometry(matrix(2,[2,0,0,1/2])) + sage: H.classification() + 'hyperbolic' + + sage: P = UHP.get_isometry(matrix(2,[1,1,0,1])) + sage: P.classification() + 'parabolic' + + sage: E = UHP.get_isometry(matrix(2,[-1,0,0,1])) + sage: E.classification() + 'reflection' + """ + return self._cached_isometry.classification() + + def translation_length(self): + r""" + For hyperbolic elements, return the translation length; + otherwise, raise a ``ValueError``. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: H = UHP.get_isometry(matrix(2,[2,0,0,1/2])) + sage: H.translation_length() + 2*arccosh(5/4) + + :: + + sage: f_1 = UHP.get_point(-1) + sage: f_2 = UHP.get_point(1) + sage: H = UHP.isometry_from_fixed_points(f_1, f_2) + sage: p = UHP.get_point(exp(i*7*pi/8)) + sage: bool((p.dist(H*p) - H.translation_length()) < 10**-9) + True + """ + return self._cached_isometry.translation_length() + + def axis(self): + r""" + For a hyperbolic isometry, return the axis of the + transformation; otherwise raise a ``ValueError``. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: H = UHP.get_isometry(matrix(2,[2,0,0,1/2])) + sage: H.axis() + Geodesic in UHP from 0 to +Infinity + + It is an error to call this function on an isometry that is + not hyperbolic:: + + sage: P = UHP.get_isometry(matrix(2,[1,4,0,1])) + sage: P.axis() + Traceback (most recent call last): + ... + ValueError: the isometry is not hyperbolic: axis is undefined + """ + if self.classification() not in ['hyperbolic', + 'orientation-reversing hyperbolic']: + raise ValueError("the isometry is not hyperbolic: axis is undefined") + return self.fixed_point_set() + + def fixed_point_set(self): + r""" + Return the a list containing the fixed point set of orientation- + preserving isometries. + + OUTPUT: + + - a list of hyperbolic points or a hyperbolic geodesic + + EXAMPLES:: + + sage: KM = HyperbolicPlane().KM() + sage: H = KM.get_isometry(matrix([[5/3,0,4/3], [0,1,0], [4/3,0,5/3]])) + sage: g = H.fixed_point_set(); g + Geodesic in KM from (1, 0) to (-1, 0) + sage: H(g.start()) == g.start() + True + sage: H(g.end()) == g.end() + True + sage: A = KM.get_isometry(matrix([[1,0,0], [0,-1,0], [0,0,1]])) + sage: A.preserves_orientation() + False + sage: A.fixed_point_set() + Geodesic in KM from (1, 0) to (-1, 0) + + :: + + sage: B = KM.get_isometry(identity_matrix(3)) + sage: B.fixed_point_set() + Traceback (most recent call last): + ... + ValueError: the identity transformation fixes the entire hyperbolic plane + """ + M = self.domain() + pts = self._cached_isometry.fixed_point_set() + if isinstance(pts, HyperbolicGeodesic): + return pts.to_model(M) + return [M(k) for k in pts] + + def fixed_geodesic(self): + r""" + If ``self`` is a reflection in a geodesic, return that geodesic. + + EXAMPLES:: + + sage: A = HyperbolicPlane().PD().get_isometry(matrix([[0, 1], [1, 0]])) + sage: A.fixed_geodesic() + Geodesic in PD from -1 to 1 + """ + fps = self._cached_isometry.fixed_point_set() + if not isinstance(fps, HyperbolicGeodesic): + raise ValueError("isometries of type {0}".format(self.classification()) + + " do not fix geodesics") + return fps.to_model(self.domain()) + + def repelling_fixed_point(self): + r""" + For a hyperbolic isometry, return the attracting fixed point; + otherwise raise a ``ValueError``. + + OUTPUT: + + - a hyperbolic point + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: A = UHP.get_isometry(Matrix(2,[4,0,0,1/4])) + sage: A.repelling_fixed_point() + Boundary point in UHP 0 + """ + fp = self._cached_isometry.repelling_fixed_point() + return self.domain().get_point(fp) + + def attracting_fixed_point(self): + r""" + For a hyperbolic isometry, return the attracting fixed point; + otherwise raise a `ValueError``. + + OUTPUT: + + - a hyperbolic point + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: A = UHP.get_isometry(Matrix(2,[4,0,0,1/4])) + sage: A.attracting_fixed_point() + Boundary point in UHP +Infinity + """ + fp = self._cached_isometry.attracting_fixed_point() + return self.domain().get_point(fp) + +class HyperbolicIsometryUHP(HyperbolicIsometry): + r""" + Create a hyperbolic isometry in the UHP model. + + INPUT: + + - a matrix in `GL(2, \RR)` + + EXAMPLES:: + + sage: HyperbolicPlane().UHP().get_isometry(identity_matrix(2)) + Isometry in UHP + [1 0] + [0 1] + """ + def _call_(self, p): #UHP + r""" + Return image of ``p`` under the action of ``self``. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: I2 = UHP.get_isometry(identity_matrix(2)) + sage: p = UHP.random_point() + sage: bool(UHP.dist(I2(p), p) < 10**-9) + True + """ + return self.codomain().get_point(mobius_transform(self._matrix, p.coordinates())) + + def preserves_orientation(self): #UHP + r""" + Return ``True`` if ``self`` is orientation preserving and ``False`` + otherwise. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: A = identity_matrix(2) + sage: UHP.get_isometry(A).preserves_orientation() + True + sage: B = matrix(2,[0,1,1,0]) + sage: UHP.get_isometry(B).preserves_orientation() + False + """ + return bool(self._matrix.det() > 0) + + def classification(self): #UHP + r""" + Classify the hyperbolic isometry as elliptic, parabolic, or + hyperbolic. + + A hyperbolic isometry fixes two points on the boundary of + hyperbolic space, a parabolic isometry fixes one point on the + boundary of hyperbolic space, and an elliptic isometry fixes + no points. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: UHP.get_isometry(identity_matrix(2)).classification() + 'identity' + + sage: UHP.get_isometry(4*identity_matrix(2)).classification() + 'identity' + + sage: UHP.get_isometry(matrix(2,[2,0,0,1/2])).classification() + 'hyperbolic' + + sage: UHP.get_isometry(matrix(2, [0, 3, -1/3, 6])).classification() + 'hyperbolic' + + sage: UHP.get_isometry(matrix(2,[1,1,0,1])).classification() + 'parabolic' + + sage: UHP.get_isometry(matrix(2,[-1,0,0,1])).classification() + 'reflection' + """ + A = self._matrix.n() + A = A / (abs(A.det()).sqrt()) + tau = abs(A.trace()) + a = A.list() + if A.det() > 0: + tf = bool((a[0] - 1)**2 + a[1]**2 + a[2]**2 + (a[3] - 1)**2 < EPSILON) + if tf: + return 'identity' + if tau - 2 < -EPSILON: + return 'elliptic' + if tau - 2 > -EPSILON and tau - 2 < EPSILON: + return 'parabolic' + if tau - 2 > EPSILON: + return 'hyperbolic' + raise ValueError("something went wrong with classification:" + + " trace is {}".format(A.trace())) + # Otherwise The isometry reverses orientation + if tau < EPSILON: + return 'reflection' + return 'orientation-reversing hyperbolic' + + def translation_length(self): #UHP + r""" + For hyperbolic elements, return the translation length; + otherwise, raise a ``ValueError``. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: UHP.get_isometry(matrix(2,[2,0,0,1/2])).translation_length() + 2*arccosh(5/4) + + :: + + sage: H = UHP.isometry_from_fixed_points(-1,1) + sage: p = UHP.get_point(exp(i*7*pi/8)) + sage: Hp = H(p) + sage: bool((UHP.dist(p, Hp) - H.translation_length()) < 10**-9) + True + """ + d = sqrt(self._matrix.det()**2) + tau = sqrt((self._matrix / sqrt(d)).trace()**2) + if self.classification() in ['hyperbolic', 'oriention-reversing hyperbolic']: + return 2 * arccosh(tau/2) + raise TypeError("translation length is only defined for hyperbolic transformations") + + def fixed_point_set(self): #UHP + r""" + Return the a list or geodesic containing the fixed point set of + orientation-preserving isometries. + + OUTPUT: + + - a list of hyperbolic points or a hyperbolic geodesic + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: H = UHP.get_isometry(matrix(2, [-2/3,-1/3,-1/3,-2/3])) + sage: g = H.fixed_point_set(); g + Geodesic in UHP from -1 to 1 + sage: H(g.start()) == g.start() + True + sage: H(g.end()) == g.end() + True + sage: A = UHP.get_isometry(matrix(2,[0,1,1,0])) + sage: A.preserves_orientation() + False + sage: A.fixed_point_set() + Geodesic in UHP from 1 to -1 + + :: + + sage: B = UHP.get_isometry(identity_matrix(2)) + sage: B.fixed_point_set() + Traceback (most recent call last): + ... + ValueError: the identity transformation fixes the entire hyperbolic plane + """ + d = sqrt(self._matrix.det() ** 2) + M = self._matrix / sqrt(d) + tau = M.trace() ** 2 + M_cls = self.classification() + if M_cls == 'identity': + raise ValueError("the identity transformation fixes the entire " + "hyperbolic plane") + + pt = self.domain().get_point + if M_cls == 'parabolic': + if abs(M[1, 0]) < EPSILON: + return [pt(infinity)] + else: + # boundary point + return [pt((M[0,0] - M[1,1]) / (2*M[1,0]))] + elif M_cls == 'elliptic': + d = sqrt(tau - 4) + return [pt((M[0,0] - M[1,1] + sign(M[1,0])*d) / (2*M[1,0]))] + elif M_cls == 'hyperbolic': + if M[1,0] != 0: #if the isometry doesn't fix infinity + d = sqrt(tau - 4) + p_1 = (M[0,0] - M[1,1]+d) / (2*M[1,0]) + p_2 = (M[0,0] - M[1,1]-d) / (2*M[1,0]) + return self.domain().get_geodesic(pt(p_1), pt(p_2)) + #else, it fixes infinity. + p_1 = M[0,1] / (M[1,1] - M[0,0]) + p_2 = infinity + return self.domain().get_geodesic(pt(p_1), pt(p_2)) + + try: + p, q = [M.eigenvectors_right()[k][1][0] for k in range(2)] + except IndexError: + M = M.change_ring(RDF) + p, q = [M.eigenvectors_right()[k][1][0] for k in range(2)] + + pts = [] + if p[1] == 0: + pts.append(infinity) + else: + p = p[0] / p[1] + if imag(p) >= 0: + pts.append(p) + if q[1] == 0: + pts.append(infinity) + else: + q = q[0] / q[1] + if imag(q) >= 0: + pts.append(q) + pts = [pt(k) for k in pts] + if len(pts) == 2: + return self.domain().get_geodesic(*pts) + return pts + + def repelling_fixed_point(self): #UHP + r""" + Return the repelling fixed point; otherwise raise a ``ValueError``. + + OUTPUT: + + - a hyperbolic point + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: A = matrix(2,[4,0,0,1/4]) + sage: UHP.get_isometry(A).repelling_fixed_point() + Boundary point in UHP 0 + """ + if self.classification() not in ['hyperbolic', + 'orientation-reversing hyperbolic']: + raise ValueError("repelling fixed point is defined only" + + "for hyperbolic isometries") + v = self._matrix.eigenmatrix_right()[1].column(1) + if v[1] == 0: + return self.domain().get_point(infinity) + return self.domain().get_point(v[0] / v[1]) + + def attracting_fixed_point(self): #UHP + r""" + Return the attracting fixed point; otherwise raise a ``ValueError``. + + OUTPUT: + + - a hyperbolic point + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: A = matrix(2,[4,0,0,1/4]) + sage: UHP.get_isometry(A).attracting_fixed_point() + Boundary point in UHP +Infinity + """ + if self.classification() not in \ + ['hyperbolic', 'orientation-reversing hyperbolic']: + raise ValueError("Attracting fixed point is defined only" + + "for hyperbolic isometries.") + v = self._matrix.eigenmatrix_right()[1].column(0) + if v[1] == 0: + return self.domain().get_point(infinity) + return self.domain().get_point(v[0] / v[1]) + +class HyperbolicIsometryPD(HyperbolicIsometry): + r""" + Create a hyperbolic isometry in the PD model. + + INPUT: + + - a matrix in `PU(1,1)` + + EXAMPLES:: + + sage: HyperbolicPlane().PD().get_isometry(identity_matrix(2)) + Isometry in PD + [1 0] + [0 1] + """ + def _call_(self, p): #PD + r""" + Return image of ``p`` under the action of ``self``. + + EXAMPLES:: + + sage: PD = HyperbolicPlane().PD() + sage: I2 = PD.get_isometry(identity_matrix(2)) + sage: q = PD.random_point() + sage: bool(PD.dist(I2(q), q) < 10**-9) + True + """ + _image = mobius_transform(self._matrix, p.coordinates()) + return self.codomain().get_point(_image) + + def __mul__(self, other): #PD + r""" + Return image of ``p`` under the action of ``self``. + + EXAMPLES:: + + sage: PD = HyperbolicPlane().PD() + sage: X = PD.get_isometry(matrix([[3/4, -I/4], [-I/4, -3/4]])) + sage: X*X + Isometry in PD + [ 5/8 3/8*I] + [-3/8*I 5/8] + + """ + if isinstance(other, HyperbolicIsometry): + M = self._cached_isometry*other._cached_isometry + return M.to_model('PD') + return super(HyperbolicIsometryPD, self).__mul__(other) + + def __pow__(self, n): #PD + r""" + EXAMPLES:: + + sage: PD = HyperbolicPlane().PD() + sage: X = PD.get_isometry(matrix([[3/4, -I/4], [-I/4, -3/4]])) + sage: X^2 + Isometry in PD + [ 5/8 3/8*I] + [-3/8*I 5/8] + + """ + return (self._cached_isometry**n).to_model('PD') + + def preserves_orientation(self): #PD + """ + Return ``True`` if ``self`` preserves orientation and ``False`` + otherwise. + + EXAMPLES:: + + sage: PD = HyperbolicPlane().PD() + sage: PD.get_isometry(matrix([[-I, 0], [0, I]])).preserves_orientation() + True + sage: PD.get_isometry(matrix([[0, I], [I, 0]])).preserves_orientation() + False + """ + return bool(self._matrix.det() > 0) and HyperbolicIsometryPD._orientation_preserving(self._matrix) + + @staticmethod + def _orientation_preserving(A): #PD + r""" + For a matrix ``A`` of a PD isometry, determine if it preserves + orientation. + + This test is more involved than just checking the sign of + the determinant. + + EXAMPLES:: + + sage: from sage.geometry.hyperbolic_space.hyperbolic_isometry import HyperbolicIsometryPD + sage: orient = HyperbolicIsometryPD._orientation_preserving + sage: orient(matrix([[-I, 0], [0, I]])) + True + sage: orient(matrix([[0, I], [I, 0]])) + False + """ + return bool(A[1][0] == A[0][1].conjugate() and A[1][1] == A[0][0].conjugate() + and abs(A[0][0]) - abs(A[0][1]) != 0) + +class HyperbolicIsometryKM(HyperbolicIsometry): + r""" + Create a hyperbolic isometry in the KM model. + + INPUT: + + - a matrix in `SO(2,1)` + + EXAMPLES:: + + sage: HyperbolicPlane().KM().get_isometry(identity_matrix(3)) + Isometry in KM + [1 0 0] + [0 1 0] + [0 0 1] + """ + def _call_(self, p): #KM + r""" + Return image of ``p`` under the action of ``self``. + + EXAMPLES:: + + sage: KM = HyperbolicPlane().KM() + sage: I3 = KM.get_isometry(identity_matrix(3)) + sage: v = KM.random_point() + sage: bool(KM.dist(I3(v), v) < 10**-9) + True + """ + v = self._matrix * vector(list(p.coordinates()) + [1]) + if v[2] == 0: + return self.codomain().get_point(infinity) + return self.codomain().get_point(v[0:2] / v[2]) + +##################################################################### +## Helper functions + +def mobius_transform(A, z): + r""" + Given a matrix ``A`` in `GL(2, \CC)` and a point ``z`` in the complex + plane return the mobius transformation action of ``A`` on ``z``. + + INPUT: + + - ``A`` -- a `2 \times 2` invertible matrix over the complex numbers + - ``z`` -- a complex number or infinity + + OUTPUT: + + - a complex number or infinity + + EXAMPLES:: + + sage: from sage.geometry.hyperbolic_space.hyperbolic_model import mobius_transform + sage: mobius_transform(matrix(2,[1,2,3,4]),2 + I) + 2/109*I + 43/109 + sage: y = var('y') + sage: mobius_transform(matrix(2,[1,0,0,1]),x + I*y) + x + I*y + + The matrix must be square and `2 \times 2`:: + + sage: mobius_transform(matrix([[3,1,2],[1,2,5]]),I) + Traceback (most recent call last): + ... + TypeError: A must be an invertible 2x2 matrix over the complex numbers or a symbolic ring + + sage: mobius_transform(identity_matrix(3),I) + Traceback (most recent call last): + ... + TypeError: A must be an invertible 2x2 matrix over the complex numbers or a symbolic ring + + The matrix can be symbolic or can be a matrix over the real + or complex numbers, but must be invertible:: + + sage: (a,b,c,d) = var('a,b,c,d'); + sage: mobius_transform(matrix(2,[a,b,c,d]),I) + (I*a + b)/(I*c + d) + + sage: mobius_transform(matrix(2,[0,0,0,0]),I) + Traceback (most recent call last): + ... + TypeError: A must be an invertible 2x2 matrix over the complex numbers or a symbolic ring + """ + if A.ncols() == 2 and A.nrows() == 2 and A.det() != 0: + (a, b, c, d) = A.list() + if z == infinity: + if c == 0: + return infinity + return a/c + if a*d - b*c < 0: + w = z.conjugate() # Reverses orientation + else: + w = z + if c*z + d == 0: + return infinity + return (a*w + b) / (c*w + d) + raise TypeError("A must be an invertible 2x2 matrix over the" + " complex numbers or a symbolic ring") diff --git a/src/sage/geometry/hyperbolic_space/hyperbolic_model.py b/src/sage/geometry/hyperbolic_space/hyperbolic_model.py new file mode 100644 index 00000000000..592b168171d --- /dev/null +++ b/src/sage/geometry/hyperbolic_space/hyperbolic_model.py @@ -0,0 +1,1486 @@ +# -*- coding: utf-8 -*- +r""" +Hyperbolic Models + +In this module, a hyperbolic model is a collection of data that allow +the user to implement new models of hyperbolic space with minimal effort. +The data include facts about the underlying set (such as whether the +model is bounded), facts about the metric (such as whether the model is +conformal), facts about the isometry group (such as whether it is a +linear or projective group), and more. Generally speaking, any data +or method that pertains to the model itself -- rather than the points, +geodesics, or isometries of the model -- is implemented in this module. + +Abstractly, a model of hyperbolic space is a connected, simply connected +manifold equipped with a complete Riemannian metric of constant curvature +`-1`. This module records information sufficient to enable computations +in hyperbolic space without explicitly specifying the underlying set or +its Riemannian metric. Although, see the +`SageManifolds `_ project if +you would like to take this approach. + +This module implements the abstract base class for a model of hyperbolic +space of arbitrary dimension. It also contains the implementations of +specific models of hyperbolic geometry. + +AUTHORS: + +- Greg Laun (2013): Initial version. + +EXAMPLES: + +We illustrate how the classes in this module encode data by comparing +the upper half plane (UHP), Poincaré disk (PD) and hyperboloid (HM) +models. First we create:: + + sage: U = HyperbolicPlane().UHP() + sage: P = HyperbolicPlane().PD() + sage: H = HyperbolicPlane().HM() + +We note that the UHP and PD models are bounded while the HM model is +not:: + + sage: U.is_bounded() and P.is_bounded() + True + sage: H.is_bounded() + False + +The isometry groups of UHP and PD are projective, while that of HM is +linear:: + + sage: U.is_isometry_group_projective() + True + sage: H.is_isometry_group_projective() + False + +The models are responsible for determining if the coordinates of points +and the matrix of linear maps are appropriate for constructing points +and isometries in hyperbolic space:: + + sage: U.point_in_model(2 + I) + True + sage: U.point_in_model(2 - I) + False + sage: U.point_in_model(2) + False + sage: U.boundary_point_in_model(2) + True + +.. TODO:: + + Implement a category for metric spaces. +""" + +#*********************************************************************** +# +# Copyright (C) 2013 Greg Laun +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# http://www.gnu.org/licenses/ +#*********************************************************************** + +from sage.structure.unique_representation import UniqueRepresentation +from sage.structure.parent import Parent +from sage.misc.bindable_class import BindableClass +from sage.misc.lazy_import import lazy_import +from sage.functions.other import imag, real, sqrt +from sage.functions.all import arccosh +from sage.rings.all import CC, RR, RDF +from sage.rings.infinity import infinity +from sage.symbolic.pynac import I +from sage.matrix.constructor import matrix +from sage.categories.homset import Hom + +from sage.geometry.hyperbolic_space.hyperbolic_constants import EPSILON, LORENTZ_GRAM +from sage.geometry.hyperbolic_space.hyperbolic_point import ( + HyperbolicPoint, HyperbolicPointUHP) +from sage.geometry.hyperbolic_space.hyperbolic_isometry import ( + HyperbolicIsometry, HyperbolicIsometryUHP, + HyperbolicIsometryPD, HyperbolicIsometryKM, mobius_transform) +from sage.geometry.hyperbolic_space.hyperbolic_geodesic import ( + HyperbolicGeodesic, HyperbolicGeodesicUHP, HyperbolicGeodesicPD, + HyperbolicGeodesicKM, HyperbolicGeodesicHM) +from sage.geometry.hyperbolic_space.hyperbolic_coercion import ( + CoercionUHPtoPD, CoercionUHPtoKM, CoercionUHPtoHM, + CoercionPDtoUHP, CoercionPDtoKM, CoercionPDtoHM, + CoercionKMtoUHP, CoercionKMtoPD, CoercionKMtoHM, + CoercionHMtoUHP, CoercionHMtoPD, CoercionHMtoKM) + +lazy_import('sage.modules.free_module_element', 'vector') + +##################################################################### +## Abstract model + +class HyperbolicModel(Parent, UniqueRepresentation, BindableClass): + r""" + Abstract base class for hyperbolic models. + """ + Element = HyperbolicPoint + _Geodesic = HyperbolicGeodesic + _Isometry = HyperbolicIsometry + + def __init__(self, space, name, short_name, bounded, conformal, + dimension, isometry_group, isometry_group_is_projective): + """ + Initialize ``self``. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: TestSuite(UHP).run() + sage: PD = HyperbolicPlane().PD() + sage: TestSuite(PD).run() + sage: KM = HyperbolicPlane().KM() + sage: TestSuite(KM).run() + sage: HM = HyperbolicPlane().HM() + sage: TestSuite(HM).run() + """ + self._name = name + self._short_name = short_name + self._bounded = bounded + self._conformal = conformal + self._dimension = dimension + self._isometry_group = isometry_group + self._isometry_group_is_projective = isometry_group_is_projective + from sage.geometry.hyperbolic_space.hyperbolic_interface import HyperbolicModels + Parent.__init__(self, category=HyperbolicModels(space)) + + def _repr_(self): # Abstract + """ + Return a string representation of ``self``. + + EXAMPLES:: + + sage: HyperbolicPlane().UHP() + Hyperbolic plane in the Upper Half Plane Model model + """ + return u'Hyperbolic plane in the {} model'.format(self._name) + + def _element_constructor_(self, x, is_boundary=None, **graphics_options): #Abstract + """ + Construct an element of ``self``. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: UHP(2 + I) + Point in UHP I + 2 + """ + return self.get_point(x, is_boundary, **graphics_options) + + def name(self): # Abstract + """ + Return the name of this model. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: UHP.name() + 'Upper Half Plane Model' + """ + return self._name + + def short_name(self): + """ + Return the short name of this model. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: UHP.short_name() + 'UHP' + """ + return self._short_name + + def is_bounded(self): + """ + Return ``True`` if ``self`` is a bounded model. + + EXAMPLES:: + + sage: HyperbolicPlane().UHP().is_bounded() + True + sage: HyperbolicPlane().PD().is_bounded() + True + sage: HyperbolicPlane().KM().is_bounded() + True + sage: HyperbolicPlane().HM().is_bounded() + False + """ + return self._bounded + + def is_conformal(self): + """ + Return ``True`` if ``self`` is a conformal model. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: UHP.is_conformal() + True + """ + return self._conformal + + def is_isometry_group_projective(self): + """ + Return ``True`` if the isometry group of ``self`` is projective. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: UHP.is_isometry_group_projective() + True + """ + return self._isometry_group_is_projective + + def point_in_model(self, p): + r""" + Return ``True`` if the point ``p`` is in the interiror of the + given model and ``False`` otherwise. + + INPUT: + + - any object that can converted into a complex number + + OUTPUT: + + - boolean + + EXAMPLES:: + + sage: HyperbolicPlane().UHP().point_in_model(I) + True + sage: HyperbolicPlane().UHP().point_in_model(-I) + False + """ + return True + + def point_test(self, p): #Abstract + r""" + Test whether a point is in the model. If the point is in the + model, do nothing. Otherwise, raise a ``ValueError``. + + EXAMPLES:: + + sage: from sage.geometry.hyperbolic_space.hyperbolic_model import HyperbolicModelUHP + sage: HyperbolicPlane().UHP().point_test(2 + I) + sage: HyperbolicPlane().UHP().point_test(2 - I) + Traceback (most recent call last): + ... + ValueError: -I + 2 is not a valid point in the UHP model + """ + if not (self.point_in_model(p) or self.boundary_point_in_model(p)): + error_string = "{0} is not a valid point in the {1} model" + raise ValueError(error_string.format(p, self._short_name)) + + def boundary_point_in_model(self, p): #Abstract + r""" + Return ``True`` if the point is on the ideal boundary of hyperbolic + space and ``False`` otherwise. + + INPUT: + + - any object that can converted into a complex number + + OUTPUT: + + - boolean + + EXAMPLES:: + + sage: HyperbolicPlane().UHP().boundary_point_in_model(I) + False + """ + return True + + def bdry_point_test(self, p): #Abstract + r""" + Test whether a point is in the model. If the point is in the + model, do nothing; otherwise raise a ``ValueError``. + + EXAMPLES:: + + sage: HyperbolicPlane().UHP().bdry_point_test(2) + sage: HyperbolicPlane().UHP().bdry_point_test(1 + I) + Traceback (most recent call last): + ... + ValueError: I + 1 is not a valid boundary point in the UHP model + """ + if not self._bounded or not self.boundary_point_in_model(p): + error_string = "{0} is not a valid boundary point in the {1} model" + raise ValueError(error_string.format(p, self._short_name)) + + def isometry_in_model(self, A): #Abstract + r""" + Return ``True`` if the input matrix represents an isometry of the + given model and ``False`` otherwise. + + INPUT: + + - a matrix that represents an isometry in the appropriate model + + OUTPUT: + + - boolean + + EXAMPLES:: + + sage: HyperbolicPlane().UHP().isometry_in_model(identity_matrix(2)) + True + + sage: HyperbolicPlane().UHP().isometry_in_model(identity_matrix(3)) + False + """ + return True + + def isometry_test(self, A): #Abstract + r""" + Test whether an isometry ``A`` is in the model. + + If the isometry is in the model, do nothing. Otherwise, raise + a ``ValueError``. + + EXAMPLES:: + + sage: HyperbolicPlane().UHP().isometry_test(identity_matrix(2)) + sage: HyperbolicPlane().UHP().isometry_test(matrix(2, [I,1,2,1])) + Traceback (most recent call last): + ... + ValueError: + [I 1] + [2 1] is not a valid isometry in the UHP model + """ + if not self.isometry_in_model(A): + error_string = "\n{0} is not a valid isometry in the {1} model" + raise ValueError(error_string.format(A, self._short_name)) + + def get_point(self, coordinates, is_boundary=None, **graphics_options): + r""" + Return a point in ``self``. + + Automatically determine the type of point to return given either: + + #. the coordinates of a point in the interior or ideal boundary + of hyperbolic space, or + #. a :class:`~sage.geometry.hyperbolic_space.hyperbolic_point.HyperbolicPoint` object. + + INPUT: + + - a point in hyperbolic space or on the ideal boundary + + OUTPUT: + + - a :class:`~sage.geometry.hyperbolic_space.hyperbolic_point.HyperbolicPoint` + + EXAMPLES: + + We can create an interior point via the coordinates:: + + sage: HyperbolicPlane().UHP().get_point(2*I) + Point in UHP 2*I + + Or we can create a boundary point via the coordinates:: + + sage: HyperbolicPlane().UHP().get_point(23) + Boundary point in UHP 23 + + However we cannot create points outside of our model:: + + sage: HyperbolicPlane().UHP().get_point(12 - I) + Traceback (most recent call last): + ... + ValueError: -I + 12 is not a valid point in the UHP model + + :: + + sage: HyperbolicPlane().UHP().get_point(2 + 3*I) + Point in UHP 3*I + 2 + + sage: HyperbolicPlane().PD().get_point(0) + Point in PD 0 + + sage: HyperbolicPlane().KM().get_point((0,0)) + Point in KM (0, 0) + + sage: HyperbolicPlane().HM().get_point((0,0,1)) + Point in HM (0, 0, 1) + + sage: p = HyperbolicPlane().UHP().get_point(I, color="red") + sage: p.graphics_options() + {'color': 'red'} + + :: + + sage: HyperbolicPlane().UHP().get_point(12) + Boundary point in UHP 12 + + sage: HyperbolicPlane().UHP().get_point(infinity) + Boundary point in UHP +Infinity + + sage: HyperbolicPlane().PD().get_point(I) + Boundary point in PD I + + sage: HyperbolicPlane().KM().get_point((0,-1)) + Boundary point in KM (0, -1) + """ + + if isinstance(coordinates, HyperbolicPoint): + if coordinates.parent() is not self: + coordinates = self(coordinates) + coordinates.update_graphics(True, **graphics_options) + return coordinates #both Point and BdryPoint + + if is_boundary is None: + is_boundary = self.boundary_point_in_model(coordinates) + return self.element_class(self, coordinates, is_boundary, **graphics_options) + + def get_geodesic(self, start, end=None, **graphics_options): #Abstract + r""" + Return a geodesic in the appropriate model. + + EXAMPLES:: + + sage: HyperbolicPlane().UHP().get_geodesic(I, 2*I) + Geodesic in UHP from I to 2*I + + sage: HyperbolicPlane().PD().get_geodesic(0, I/2) + Geodesic in PD from 0 to 1/2*I + + sage: HyperbolicPlane().KM().get_geodesic((1/2, 1/2), (0,0)) + Geodesic in KM from (1/2, 1/2) to (0, 0) + + sage: HyperbolicPlane().HM().get_geodesic((0,0,1), (1,0, sqrt(2))) + Geodesic in HM from (0, 0, 1) to (1, 0, sqrt(2)) + + TESTS:: + + sage: UHP = HyperbolicPlane().UHP() + sage: g = UHP.get_geodesic(UHP.get_point(I), UHP.get_point(2 + I)) + sage: h = UHP.get_geodesic(I, 2 + I) + sage: g == h + True + """ + if end is None: + if isinstance(start, HyperbolicGeodesic): + G = start + if G.model() is not self: + G = G.to_model(self) + G.update_graphics(True, **graphics_options) + return G + raise ValueError("the start and end points must be specified") + return self._Geodesic(self, self(start), self(end), **graphics_options) + + def get_isometry(self, A): + r""" + Return an isometry in ``self`` from the matrix ``A`` in the + isometry group of ``self``. + + EXAMPLES:: + + sage: HyperbolicPlane().UHP().get_isometry(identity_matrix(2)) + Isometry in UHP + [1 0] + [0 1] + + sage: HyperbolicPlane().PD().get_isometry(identity_matrix(2)) + Isometry in PD + [1 0] + [0 1] + + sage: HyperbolicPlane().KM().get_isometry(identity_matrix(3)) + Isometry in KM + [1 0 0] + [0 1 0] + [0 0 1] + + sage: HyperbolicPlane().HM().get_isometry(identity_matrix(3)) + Isometry in HM + [1 0 0] + [0 1 0] + [0 0 1] + """ + if isinstance(A, HyperbolicIsometry): + if A.model() is not self: + return A.to_model(self) + return A + return self._Isometry(self, A) + + def random_element(self, **kwargs): + r""" + Return a random point in ``self``. + + The points are uniformly distributed over the rectangle + `[-10, 10] \times [0, 10 i]` in the upper half plane model. + + EXAMPLES:: + + sage: p = HyperbolicPlane().UHP().random_element() + sage: bool((p.coordinates().imag()) > 0) + True + + sage: p = HyperbolicPlane().PD().random_element() + sage: HyperbolicPlane().PD().point_in_model(p.coordinates()) + True + + sage: p = HyperbolicPlane().KM().random_element() + sage: HyperbolicPlane().KM().point_in_model(p.coordinates()) + True + + sage: p = HyperbolicPlane().HM().random_element().coordinates() + sage: bool((p[0]**2 + p[1]**2 - p[2]**2 - 1) < 10**-8) + True + """ + return self.random_point(**kwargs) + + def random_point(self, **kwargs): + r""" + Return a random point of ``self``. + + The points are uniformly distributed over the rectangle + `[-10, 10] \times [0, 10 i]` in the upper half plane model. + + EXAMPLES:: + + sage: p = HyperbolicPlane().UHP().random_point() + sage: bool((p.coordinates().imag()) > 0) + True + + sage: PD = HyperbolicPlane().PD() + sage: p = PD.random_point() + sage: PD.point_in_model(p.coordinates()) + True + """ + R = self.realization_of().a_realization() + return self(R.random_point(**kwargs)) + + def random_geodesic(self, **kwargs): + r""" + Return a random hyperbolic geodesic. + + Return the geodesic between two random points. + + EXAMPLES:: + + sage: h = HyperbolicPlane().PD().random_geodesic() + sage: bool((h.endpoints()[0].coordinates()).imag() >= 0) + True + """ + R = self.realization_of().a_realization() + g_ends = [R.random_point(**kwargs) for k in range(2)] + return self.get_geodesic(self(g_ends[0]), self(g_ends[1])) + + def random_isometry(self, preserve_orientation=True, **kwargs): + r""" + Return a random isometry in the model of ``self``. + + INPUT: + + - ``preserve_orientation`` -- if ``True`` return an + orientation-preserving isometry + + OUTPUT: + + - a hyperbolic isometry + + EXAMPLES:: + + sage: A = HyperbolicPlane().PD().random_isometry() + sage: A.preserves_orientation() + True + sage: B = HyperbolicPlane().PD().random_isometry(preserve_orientation=False) + sage: B.preserves_orientation() + False + """ + R = self.realization_of().a_realization() + A = R.random_isometry(preserve_orientation, **kwargs) + return A.to_model(self) + + ################ + # Dist methods # + ################ + + def dist(self, a, b): + r""" + Calculate the hyperbolic distance between ``a`` and ``b``. + + INPUT: + + - ``a``, ``b`` -- a point or geodesic + + OUTPUT: + + - the hyperbolic distance + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: p1 = UHP.get_point(5 + 7*I) + sage: p2 = UHP.get_point(1.0 + I) + sage: UHP.dist(p1, p2) + 2.23230104635820 + + sage: PD = HyperbolicPlane().PD() + sage: p1 = PD.get_point(0) + sage: p2 = PD.get_point(I/2) + sage: PD.dist(p1, p2) + arccosh(5/3) + + sage: UHP(p1).dist(UHP(p2)) + arccosh(5/3) + + sage: KM = HyperbolicPlane().KM() + sage: p1 = KM.get_point((0, 0)) + sage: p2 = KM.get_point((1/2, 1/2)) + sage: numerical_approx(KM.dist(p1, p2)) + 0.881373587019543 + + sage: HM = HyperbolicPlane().HM() + sage: p1 = HM.get_point((0,0,1)) + sage: p2 = HM.get_point((1,0,sqrt(2))) + sage: numerical_approx(HM.dist(p1, p2)) + 0.881373587019543 + + Distance between a point and itself is 0:: + + sage: p = UHP.get_point(47 + I) + sage: UHP.dist(p, p) + 0 + + Points on the boundary are infinitely far from interior points:: + + sage: UHP.get_point(3).dist(UHP.get_point(I)) + +Infinity + + TESTS:: + + sage: UHP.dist(UHP.get_point(I), UHP.get_point(2*I)) + arccosh(5/4) + sage: UHP.dist(I, 2*I) + arccosh(5/4) + """ + coords = lambda x: self(x).coordinates() + + if isinstance(a, HyperbolicGeodesic): + if isinstance(b, HyperbolicGeodesic): + if not a.is_parallel(b): + return 0 + + if a.is_ultra_parallel(b): + perp = a.common_perpendicular(b) + # Find where a and b intersect the common perp... + p = a.intersection(perp)[0] + q = b.intersection(perp)[0] + # ...and return their distance + return self._dist_points(coords(p), coords(q)) + + raise NotImplementedError("can only compute distance between" + " ultra-parallel and interecting geodesics") + + # If only one is a geodesic, make sure it's b to make things easier + a,b = b,a + + if isinstance(b, HyperbolicGeodesic): + (p, q) = b.ideal_endpoints() + return self._dist_geod_point(coords(p), coords(q), coords(a)) + + return self._dist_points(coords(a), coords(b)) + + def _dist_points(self, p1, p2): + r""" + Compute the distance between two points. + + INPUT: + + - ``p1``, ``p2`` -- the coordinates of the points + + EXAMPLES:: + + sage: HyperbolicPlane().PD()._dist_points(3/5*I, 0) + arccosh(17/8) + """ + R = self.realization_of().a_realization() + phi = R.coerce_map_from(self) + return R._dist_points(phi.image_coordinates(p1), phi.image_coordinates(p2)) + + def _dist_geod_point(self, start, end, p): + r""" + Return the hyperbolic distance from a given hyperbolic geodesic + and a hyperbolic point. + + INPUT: + + - ``start`` -- the start ideal point coordinates of the geodesic + - ``end`` -- the end ideal point coordinates of the geodesic + - ``p`` -- the coordinates of the point + + OUTPUT: + + - the hyperbolic distance + + EXAMPLES:: + + sage: HyperbolicPlane().PD()._dist_geod_point(3/5*I + 4/5, I, 0) + arccosh(1/10*sqrt(5)*((sqrt(5) - 1)^2 + 4) + 1) + + If `p` is a boundary point, the distance is infinity:: + + sage: HyperbolicPlane().PD()._dist_geod_point(3/5*I + 4/5, I, 12/13*I + 5/13) + +Infinity + """ + R = self.realization_of().a_realization() + assert R is not self + phi = lambda c: R.coerce_map_from(self).image_coordinates(c) + return R._dist_geod_point(phi(start), phi(end), phi(p)) + + #################### + # Isometry methods # + #################### + + def isometry_from_fixed_points(self, repel, attract): + r""" + Given two fixed points ``repel`` and ``attract`` as hyperbolic + points return a hyperbolic isometry with ``repel`` as repelling + fixed point and ``attract`` as attracting fixed point. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: PD = HyperbolicPlane().PD() + sage: PD.isometry_from_fixed_points(-i, i) + Isometry in PD + [ 3/4 1/4*I] + [-1/4*I 3/4] + + :: + + sage: p, q = PD.get_point(1/2 + I/2), PD.get_point(6/13 + 9/13*I) + sage: PD.isometry_from_fixed_points(p, q) + Traceback (most recent call last): + ... + ValueError: fixed points of hyperbolic elements must be ideal + + sage: p, q = PD.get_point(4/5 + 3/5*I), PD.get_point(-I) + sage: PD.isometry_from_fixed_points(p, q) + Isometry in PD + [ 1/6*I - 2/3 -1/3*I - 1/6] + [ 1/3*I - 1/6 -1/6*I - 2/3] + """ + R = self.realization_of().a_realization() + return R.isometry_from_fixed_points(R(self(repel)), R(self(attract))).to_model(self) + + +##################################################################### +## Upper half plane model + +class HyperbolicModelUHP(HyperbolicModel): + r""" + Upper Half Plane model. + """ + Element = HyperbolicPointUHP + _Geodesic = HyperbolicGeodesicUHP + _Isometry = HyperbolicIsometryUHP + + def __init__(self, space): + """ + Initialize ``self``. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: TestSuite(UHP).run() + """ + HyperbolicModel.__init__(self, space, + name="Upper Half Plane Model", short_name="UHP", + bounded=True, conformal=True, dimension=2, + isometry_group="PSL(2, \\RR)", isometry_group_is_projective=True) + + def _coerce_map_from_(self, X): + """ + Return if the there is a coercion map from ``X`` to ``self``. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: UHP.has_coerce_map_from(HyperbolicPlane().PD()) + True + sage: UHP.has_coerce_map_from(HyperbolicPlane().KM()) + True + sage: UHP.has_coerce_map_from(HyperbolicPlane().HM()) + True + sage: UHP.has_coerce_map_from(QQ) + False + """ + if isinstance(X, HyperbolicModelPD): + return CoercionPDtoUHP(Hom(X, self)) + if isinstance(X, HyperbolicModelKM): + return CoercionKMtoUHP(Hom(X, self)) + if isinstance(X, HyperbolicModelHM): + return CoercionHMtoUHP(Hom(X, self)) + return super(HyperbolicModelUHP, self)._coerce_map_from_(X) + + def point_in_model(self, p): + r""" + Check whether a complex number lies in the open upper half plane. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: UHP.point_in_model(1 + I) + True + sage: UHP.point_in_model(infinity) + False + sage: UHP.point_in_model(CC(infinity)) + False + sage: UHP.point_in_model(RR(infinity)) + False + sage: UHP.point_in_model(1) + False + sage: UHP.point_in_model(12) + False + sage: UHP.point_in_model(1 - I) + False + sage: UHP.point_in_model(-2*I) + False + sage: UHP.point_in_model(I) + True + sage: UHP.point_in_model(0) # Not interior point + False + """ + if isinstance(p, HyperbolicPoint): + return p.is_boundary() + return bool(imag(CC(p)) > 0) + + def boundary_point_in_model(self, p): + r""" + Check whether a complex number is a real number or ``\infty``. + In the ``UHP.model_name_name``, this is the ideal boundary of + hyperbolic space. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: UHP.boundary_point_in_model(1 + I) + False + sage: UHP.boundary_point_in_model(infinity) + True + sage: UHP.boundary_point_in_model(CC(infinity)) + True + sage: UHP.boundary_point_in_model(RR(infinity)) + True + sage: UHP.boundary_point_in_model(1) + True + sage: UHP.boundary_point_in_model(12) + True + sage: UHP.boundary_point_in_model(1 - I) + False + sage: UHP.boundary_point_in_model(-2*I) + False + sage: UHP.boundary_point_in_model(0) + True + sage: UHP.boundary_point_in_model(I) + False + """ + if isinstance(p, HyperbolicPoint): + return p.is_boundary() + im = abs(imag(CC(p)).n()) + return (im < EPSILON) or bool(p == infinity) + + def isometry_in_model(self, A): + r""" + Check that ``A`` acts as an isometry on the upper half plane. + That is, ``A`` must be an invertible `2 \times 2` matrix with real + entries. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: A = matrix(2,[1,2,3,4]) + sage: UHP.isometry_in_model(A) + True + sage: B = matrix(2,[I,2,4,1]) + sage: UHP.isometry_in_model(B) + False + + An example of a matrix `A` such that `\det(A) \neq 1`, but the `A` + acts isometrically:: + + sage: C = matrix(2,[10,0,0,10]) + sage: UHP.isometry_in_model(C) + True + """ + if isinstance(A, HyperbolicIsometry): + return True + return bool(A.ncols() == 2 and A.nrows() == 2 and + sum([k in RR for k in A.list()]) == 4 and + abs(A.det()) > -EPSILON) + + def get_background_graphic(self, **bdry_options): + r""" + Return a graphic object that makes the model easier to visualize. + For the upper half space, the background object is the ideal boundary. + + EXAMPLES:: + + sage: hp = HyperbolicPlane().UHP().get_background_graphic() + """ + from sage.plot.line import line + bd_min = bdry_options.get('bd_min', -5) + bd_max = bdry_options.get('bd_max', 5) + return line(((bd_min, 0), (bd_max, 0)), color='black') + + ################ + # Dist methods # + ################ + + def _dist_points(self, p1, p2): + r""" + Compute the distance between two points in the Upper Half Plane + using the hyperbolic metric. + + INPUT: + + - ``p1``, ``p2`` -- the coordinates of the points + + EXAMPLES:: + + sage: HyperbolicPlane().UHP()._dist_points(4.0*I, I) + 1.38629436111989 + """ + num = (real(p2) - real(p1))**2 + (imag(p2) - imag(p1))**2 + denom = 2 * imag(p1) * imag(p2) + if denom == 0: + return infinity + return arccosh(1 + num/denom) + + def _dist_geod_point(self, start, end, p): + r""" + Return the hyperbolic distance from a given hyperbolic geodesic + and a hyperbolic point. + + INPUT: + + - ``start`` -- the start ideal point coordinates of the geodesic + - ``end`` -- the end ideal point coordinates of the geodesic + - ``p`` -- the coordinates of the point + + OUTPUT: + + - the hyperbolic distance + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: UHP._dist_geod_point(2, infinity, I) + arccosh(1/10*sqrt(5)*((sqrt(5) - 1)^2 + 4) + 1) + + If `p` is a boundary point, the distance is infinity:: + + sage: HyperbolicPlane().UHP()._dist_geod_point(2, infinity, 5) + +Infinity + """ + # Here is the trick for computing distance to a geodesic: + # find an isometry mapping the geodesic to the geodesic between + # 0 and infinity (so corresponding to the line imag(z) = 0. + # then any complex number is r exp(i*theta) in polar coordinates. + # the mutual perpendicular between this point and imag(z) = 0 + # intersects imag(z) = 0 at ri. So we calculate the distance + # between r exp(i*theta) and ri after we transform the original + # point. + if start + end != infinity: + # Not a straight line: + # Map the endpoints to 0 and infinity and the midpoint to 1. + T = HyperbolicGeodesicUHP._crossratio_matrix(start, (start + end)/2, end) + else: + # Is a straight line: + # Map the endpoints to 0 and infinity and another endpoint to 1. + T = HyperbolicGeodesicUHP._crossratio_matrix(start, start + 1, end) + x = mobius_transform(T, p) + return self._dist_points(x, abs(x)*I) + + ################# + # Point Methods # + ################# + + def random_point(self, **kwargs): + r""" + Return a random point in the upper half plane. The points are + uniformly distributed over the rectangle `[-10, 10] \times [0, 10i]`. + + EXAMPLES:: + + sage: p = HyperbolicPlane().UHP().random_point().coordinates() + sage: bool((p.imag()) > 0) + True + """ + # TODO: use **kwargs to allow these to be set + real_min = -10 + real_max = 10 + imag_min = 0 + imag_max = 10 + p = RR.random_element(min=real_min, max=real_max) \ + + I * RR.random_element(min=imag_min, max=imag_max) + return self.get_point(p) + + #################### + # Isometry Methods # + #################### + + def isometry_from_fixed_points(self, repel, attract): + r""" + Given two fixed points ``repel`` and ``attract`` as complex + numbers return a hyperbolic isometry with ``repel`` as repelling + fixed point and ``attract`` as attracting fixed point. + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: UHP.isometry_from_fixed_points(2 + I, 3 + I) + Traceback (most recent call last): + ... + ValueError: fixed points of hyperbolic elements must be ideal + + sage: UHP.isometry_from_fixed_points(2, 0) + Isometry in UHP + [ -1 0] + [-1/3 -1/3] + + TESTS:: + + sage: UHP = HyperbolicPlane().UHP() + sage: UHP.isometry_from_fixed_points(0, 4) + Isometry in UHP + [ -1 0] + [-1/5 -1/5] + sage: UHP.isometry_from_fixed_points(UHP.get_point(0), UHP.get_point(4)) + Isometry in UHP + [ -1 0] + [-1/5 -1/5] + """ + if isinstance(repel, HyperbolicPoint): + repel = repel._coordinates + if isinstance(attract, HyperbolicPoint): + attract = attract._coordinates + + if imag(repel) + imag(attract) > EPSILON: + raise ValueError("fixed points of hyperbolic elements must be ideal") + repel = real(repel) + attract = real(attract) + if repel == infinity: + A = self._mobius_sending([infinity, attract, attract + 1], + [infinity, attract, attract + 2]) + elif attract == infinity: + A = self._mobius_sending([repel, infinity, repel + 1], + [repel, infinity, repel + 2]) + else: + A = self._mobius_sending([repel, attract, infinity], + [repel, attract, max(repel, attract) + 1]) + return self.get_isometry(A) + + def random_isometry(self, preserve_orientation=True, **kwargs): + r""" + Return a random isometry in the Upper Half Plane model. + + INPUT: + + - ``preserve_orientation`` -- if ``True`` return an + orientation-preserving isometry + + OUTPUT: + + - a hyperbolic isometry + + EXAMPLES:: + + sage: A = HyperbolicPlane().UHP().random_isometry() + sage: B = HyperbolicPlane().UHP().random_isometry(preserve_orientation=False) + sage: B.preserves_orientation() + False + """ + [a,b,c,d] = [RR.random_element() for k in range(4)] + while abs(a*d - b*c) < EPSILON: + [a,b,c,d] = [RR.random_element() for k in range(4)] + M = matrix(RDF, 2,[a,b,c,d]) + M = M / (M.det()).abs().sqrt() + if M.det() > 0: + if not preserve_orientation: + M = M * matrix(2,[0,1,1,0]) + elif preserve_orientation: + M = M * matrix(2,[0,1,1,0]) + return self._Isometry(self, M, check=False) + + ################### + # Helping Methods # + ################### + + @staticmethod + def _mobius_sending(z, w): #UHP + r""" + Given two lists ``z`` and ``w`` of three points each in + `\mathbb{CP}^1`, return the linear fractional transformation + taking the points in ``z`` to the points in ``w``. + + EXAMPLES:: + + sage: from sage.geometry.hyperbolic_space.hyperbolic_model import HyperbolicModelUHP + sage: from sage.geometry.hyperbolic_space.hyperbolic_isometry import mobius_transform + sage: bool(abs(mobius_transform(HyperbolicModelUHP._mobius_sending([1,2,infinity],[3 - I, 5*I,-12]),1) - 3 + I) < 10^-4) + True + sage: bool(abs(mobius_transform(HyperbolicModelUHP._mobius_sending([1,2,infinity],[3 - I, 5*I,-12]),2) - 5*I) < 10^-4) + True + sage: bool(abs(mobius_transform(HyperbolicModelUHP._mobius_sending([1,2,infinity],[3 - I, 5*I,-12]),infinity) + 12) < 10^-4) + True + """ + if len(z) != 3 or len(w) != 3: + raise TypeError("mobius_sending requires each list to be three points long") + A = HyperbolicGeodesicUHP._crossratio_matrix(z[0],z[1],z[2]) + B = HyperbolicGeodesicUHP._crossratio_matrix(w[0],w[1],w[2]) + return B.inverse() * A + +##################################################################### +## Poincaré disk model + +class HyperbolicModelPD(HyperbolicModel): + r""" + Poincaré Disk Model. + """ + _Geodesic = HyperbolicGeodesicPD + _Isometry = HyperbolicIsometryPD + + def __init__(self, space): + """ + Initialize ``self``. + + EXAMPLES:: + + sage: PD = HyperbolicPlane().PD() + sage: TestSuite(PD).run() + """ + # name should really be 'Poincaré Disk Model', but utf8 is not + # accepted by repr + HyperbolicModel.__init__(self, space, + name=u'Poincare Disk Model', short_name="PD", + bounded=True, conformal=True, dimension=2, + isometry_group="PU(1, 1)", + isometry_group_is_projective=True) + + def _coerce_map_from_(self, X): + """ + Return if the there is a coercion map from ``X`` to ``self``. + + EXAMPLES:: + + sage: PD = HyperbolicPlane().PD() + sage: PD.has_coerce_map_from(HyperbolicPlane().UHP()) + True + sage: PD.has_coerce_map_from(HyperbolicPlane().KM()) + True + sage: PD.has_coerce_map_from(HyperbolicPlane().HM()) + True + sage: PD.has_coerce_map_from(QQ) + False + """ + if isinstance(X, HyperbolicModelUHP): + return CoercionUHPtoPD(Hom(X, self)) + if isinstance(X, HyperbolicModelKM): + return CoercionKMtoPD(Hom(X, self)) + if isinstance(X, HyperbolicModelHM): + return CoercionHMtoPD(Hom(X, self)) + return super(HyperbolicModelPD, self)._coerce_map_from_(X) + + def point_in_model(self, p): + r""" + Check whether a complex number lies in the open unit disk. + + EXAMPLES:: + + sage: PD = HyperbolicPlane().PD() + sage: PD.point_in_model(1.00) + False + sage: PD.point_in_model(1/2 + I/2) + True + sage: PD.point_in_model(1 + .2*I) + False + """ + if isinstance(p, HyperbolicPoint): + return p.is_boundary() + return bool(abs(CC(p)) < 1) + + def boundary_point_in_model(self, p): + r""" + Check whether a complex number lies in the open unit disk. + + EXAMPLES:: + + sage: PD = HyperbolicPlane().PD() + sage: PD.boundary_point_in_model(1.00) + True + sage: PD.boundary_point_in_model(1/2 + I/2) + False + sage: PD.boundary_point_in_model(1 + .2*I) + False + """ + if isinstance(p, HyperbolicPoint): + return p.is_boundary() + return bool(abs(abs(CC(p)) - 1) < EPSILON) + + def isometry_in_model(self, A): + r""" + Check if the given matrix ``A`` is in the group `U(1,1)`. + + EXAMPLES:: + + sage: z = [CC.random_element() for k in range(2)]; z.sort(key=abs) + sage: A = matrix(2,[z[1], z[0],z[0].conjugate(),z[1].conjugate()]) + sage: HyperbolicPlane().PD().isometry_in_model(A) + True + """ + if isinstance(A, HyperbolicIsometry): + return True + # alpha = A[0][0] + # beta = A[0][1] + # Orientation preserving and reversing + return (HyperbolicIsometryPD._orientation_preserving(A) + or HyperbolicIsometryPD._orientation_preserving(I * A)) + + def get_background_graphic(self, **bdry_options): + r""" + Return a graphic object that makes the model easier to visualize. + + For the Poincaré disk, the background object is the ideal boundary. + + EXAMPLES:: + + sage: circ = HyperbolicPlane().PD().get_background_graphic() + """ + from sage.plot.circle import circle + return circle((0, 0), 1, axes=False, color='black') + + +##################################################################### +## Klein disk model + +class HyperbolicModelKM(HyperbolicModel): + r""" + Klein Model. + """ + _Geodesic = HyperbolicGeodesicKM + _Isometry = HyperbolicIsometryKM + + def __init__(self, space): + """ + Initialize ``self``. + + EXAMPLES:: + + sage: KM = HyperbolicPlane().KM() + sage: TestSuite(KM).run() + """ + HyperbolicModel.__init__(self, space, + name="Klein Disk Model", short_name="KM", + bounded=True, conformal=False, dimension=2, + isometry_group="PSO(2, 1)", isometry_group_is_projective=True) + + def _coerce_map_from_(self, X): + """ + Return if the there is a coercion map from ``X`` to ``self``. + + EXAMPLES:: + + sage: KM = HyperbolicPlane().UHP() + sage: KM.has_coerce_map_from(HyperbolicPlane().UHP()) + True + sage: KM.has_coerce_map_from(HyperbolicPlane().PD()) + True + sage: KM.has_coerce_map_from(HyperbolicPlane().HM()) + True + sage: KM.has_coerce_map_from(QQ) + False + """ + if isinstance(X, HyperbolicModelUHP): + return CoercionUHPtoKM(Hom(X, self)) + if isinstance(X, HyperbolicModelPD): + return CoercionPDtoKM(Hom(X, self)) + if isinstance(X, HyperbolicModelHM): + return CoercionHMtoKM(Hom(X, self)) + return super(HyperbolicModelKM, self)._coerce_map_from_(X) + + def point_in_model(self, p): + r""" + Check whether a point lies in the open unit disk. + + EXAMPLES:: + + sage: KM = HyperbolicPlane().KM() + sage: KM.point_in_model((1, 0)) + False + sage: KM.point_in_model((1/2, 1/2)) + True + sage: KM.point_in_model((1, .2)) + False + """ + if isinstance(p, HyperbolicPoint): + return p.is_boundary() + return len(p) == 2 and bool(p[0]**2 + p[1]**2 < 1) + + def boundary_point_in_model(self, p): + r""" + Check whether a point lies in the unit circle, which corresponds + to the ideal boundary of the hyperbolic plane in the Klein model. + + EXAMPLES:: + + sage: KM = HyperbolicPlane().KM() + sage: KM.boundary_point_in_model((1, 0)) + True + sage: KM.boundary_point_in_model((1/2, 1/2)) + False + sage: KM.boundary_point_in_model((1, .2)) + False + """ + if isinstance(p, HyperbolicPoint): + return p.is_boundary() + return len(p) == 2 and bool(abs(p[0]**2 + p[1]**2 - 1) < EPSILON) + + def isometry_in_model(self, A): + r""" + Check if the given matrix ``A`` is in the group `SO(2,1)`. + + EXAMPLES:: + + sage: A = matrix(3, [[1, 0, 0], [0, 17/8, 15/8], [0, 15/8, 17/8]]) + sage: HyperbolicPlane().KM().isometry_in_model(A) + True + """ + if isinstance(A, HyperbolicIsometry): + return True + return bool((A*LORENTZ_GRAM*A.transpose() - LORENTZ_GRAM).norm()**2 + < EPSILON) + + def get_background_graphic(self, **bdry_options): + r""" + Return a graphic object that makes the model easier to visualize. + For the Klein model, the background object is the ideal boundary. + + EXAMPLES:: + + sage: circ = HyperbolicPlane().KM().get_background_graphic() + """ + from sage.plot.circle import circle + return circle((0,0), 1, axes=False, color='black') + +##################################################################### +## Hyperboloid model + +class HyperbolicModelHM(HyperbolicModel): + r""" + Hyperboloid Model. + """ + _Geodesic = HyperbolicGeodesicHM + + def __init__(self, space): + """ + Initialize ``self``. + + EXAMPLES:: + + sage: HM = HyperbolicPlane().HM() + sage: TestSuite(HM).run() + """ + HyperbolicModel.__init__(self, space, + name="Hyperboloid Model", short_name="HM", + bounded=False, conformal=True, dimension=2, + isometry_group="SO(2, 1)", isometry_group_is_projective=False) + + def _coerce_map_from_(self, X): + """ + Return if the there is a coercion map from ``X`` to ``self``. + + EXAMPLES:: + + sage: HM = HyperbolicPlane().UHP() + sage: HM.has_coerce_map_from(HyperbolicPlane().UHP()) + True + sage: HM.has_coerce_map_from(HyperbolicPlane().PD()) + True + sage: HM.has_coerce_map_from(HyperbolicPlane().KM()) + True + sage: HM.has_coerce_map_from(QQ) + False + """ + if isinstance(X, HyperbolicModelUHP): + return CoercionUHPtoHM(Hom(X, self)) + if isinstance(X, HyperbolicModelPD): + return CoercionPDtoHM(Hom(X, self)) + if isinstance(X, HyperbolicModelKM): + return CoercionKMtoHM(Hom(X, self)) + return super(HyperbolicModelHM, self)._coerce_map_from_(X) + + def point_in_model(self, p): + r""" + Check whether a complex number lies in the hyperboloid. + + EXAMPLES:: + + sage: HM = HyperbolicPlane().HM() + sage: HM.point_in_model((0,0,1)) + True + sage: HM.point_in_model((1,0,sqrt(2))) + True + sage: HM.point_in_model((1,2,1)) + False + """ + if isinstance(p, HyperbolicPoint): + return p.is_boundary() + return len(p) == 3 and bool(p[0]**2 + p[1]**2 - p[2]**2 + 1 < EPSILON) + + def boundary_point_in_model(self, p): + r""" + Return ``False`` since the Hyperboloid model has no boundary points. + + EXAMPLES:: + + sage: HM = HyperbolicPlane().HM() + sage: HM.boundary_point_in_model((0,0,1)) + False + sage: HM.boundary_point_in_model((1,0,sqrt(2))) + False + sage: HM.boundary_point_in_model((1,2,1)) + False + """ + return False + + def isometry_in_model(self, A): + r""" + Test that the matrix ``A`` is in the group `SO(2,1)^+`. + + EXAMPLES:: + + sage: A = diagonal_matrix([1,1,-1]) + sage: HyperbolicPlane().HM().isometry_in_model(A) + True + """ + if isinstance(A, HyperbolicIsometry): + return True + return bool((A*LORENTZ_GRAM*A.transpose() - LORENTZ_GRAM).norm()**2 < EPSILON) + + def get_background_graphic(self, **bdry_options): + r""" + Return a graphic object that makes the model easier to visualize. + For the hyperboloid model, the background object is the hyperboloid + itself. + + EXAMPLES:: + + sage: H = HyperbolicPlane().HM().get_background_graphic() + """ + from sage.plot.plot3d.all import plot3d + from sage.all import var + hyperboloid_opacity = bdry_options.get('hyperboloid_opacity', .1) + z_height = bdry_options.get('z_height', 7.0) + x_max = sqrt((z_height ** 2 - 1) / 2.0) + (x, y) = var('x,y') + return plot3d((1 + x ** 2 + y ** 2).sqrt(), + (x, -x_max, x_max), (y,-x_max, x_max), + opacity=hyperboloid_opacity, **bdry_options) diff --git a/src/sage/geometry/hyperbolic_space/hyperbolic_point.py b/src/sage/geometry/hyperbolic_space/hyperbolic_point.py new file mode 100644 index 00000000000..42ac2138159 --- /dev/null +++ b/src/sage/geometry/hyperbolic_space/hyperbolic_point.py @@ -0,0 +1,612 @@ +r""" +Hyperbolic Points + +This module implements points in hyperbolic space of arbitrary dimension. +It also contains the implementations for specific models of +hyperbolic geometry. + +This module also implements ideal points in hyperbolic space of arbitrary +dimension. It also contains the implementations for specific models +of hyperbolic geometry. + +Note that not all models of hyperbolic space are bounded, meaning that +the ideal boundary is not the topological boundary of the set underlying +tho model. For example, the unit disk model is bounded with boundary +given by the unit sphere. The hyperboloid model is not bounded. + +AUTHORS: + +- Greg Laun (2013): initial version + +EXAMPLES: + +We can construct points in the upper half plane model, abbreviated +UHP for convenience:: + + sage: UHP = HyperbolicPlane().UHP() + sage: UHP.get_point(2 + I) + Point in UHP I + 2 + sage: g = UHP.get_point(3 + I) + sage: g.dist(UHP.get_point(I)) + arccosh(11/2) + +We can also construct boundary points in the upper half plane model:: + + sage: UHP.get_point(3) + Boundary point in UHP 3 + +Some more examples:: + + sage: HyperbolicPlane().UHP().get_point(0) + Boundary point in UHP 0 + + sage: HyperbolicPlane().PD().get_point(I/2) + Point in PD 1/2*I + + sage: HyperbolicPlane().KM().get_point((0,1)) + Boundary point in KM (0, 1) + + sage: HyperbolicPlane().HM().get_point((0,0,1)) + Point in HM (0, 0, 1) +""" + +#*********************************************************************** +# Copyright (C) 2013 Greg Laun +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# http://www.gnu.org/licenses/ +#*********************************************************************** + +from sage.structure.element import Element +from sage.symbolic.pynac import I +from sage.misc.latex import latex +from sage.matrix.matrix import is_Matrix +from sage.matrix.constructor import matrix +from sage.modules.free_module_element import vector +from sage.rings.infinity import infinity +from sage.rings.all import RR, CC +from sage.functions.other import real, imag + +from sage.geometry.hyperbolic_space.hyperbolic_isometry import HyperbolicIsometry + +class HyperbolicPoint(Element): + r""" + Abstract base class for hyperbolic points. This class should never + be instantiated. + + INPUT: + + - ``model`` -- the model of the hyperbolic space + - ``coordinates`` -- the coordinates of a hyperbolic point in the + appropriate model + - ``is_boundary`` -- whether the point is a boundary point + - ``check`` -- (default: ``True``) if ``True``, then check to make sure + the coordinates give a valid point the the model + + EXAMPLES: + + Note that the coordinate representation does not differentiate the + different models:: + + sage: p = HyperbolicPlane().UHP().get_point(.2 + .3*I); p + Point in UHP 0.200000000000000 + 0.300000000000000*I + + sage: q = HyperbolicPlane().PD().get_point(0.2 + 0.3*I); q + Point in PD 0.200000000000000 + 0.300000000000000*I + + sage: p == q + False + + sage: bool(p.coordinates() == q.coordinates()) + True + + Similarly for boundary points:: + + sage: p = HyperbolicPlane().UHP().get_point(1); p + Boundary point in UHP 1 + + sage: q = HyperbolicPlane().PD().get_point(1); q + Boundary point in PD 1 + + sage: p == q + False + + sage: bool(p.coordinates() == q.coordinates()) + True + + It is an error to specify a point that does not lie in the + appropriate model:: + + sage: HyperbolicPlane().UHP().get_point(0.2 - 0.3*I) + Traceback (most recent call last): + ... + ValueError: 0.200000000000000 - 0.300000000000000*I is not a valid point in the UHP model + + sage: HyperbolicPlane().PD().get_point(1.2) + Traceback (most recent call last): + ... + ValueError: 1.20000000000000 is not a valid point in the PD model + + sage: HyperbolicPlane().KM().get_point((1,1)) + Traceback (most recent call last): + ... + ValueError: (1, 1) is not a valid point in the KM model + + sage: HyperbolicPlane().HM().get_point((1, 1, 1)) + Traceback (most recent call last): + ... + ValueError: (1, 1, 1) is not a valid point in the HM model + + It is an error to specify an interior point of hyperbolic space as a + boundary point:: + + sage: HyperbolicPlane().UHP().get_point(0.2 + 0.3*I, is_boundary=True) + Traceback (most recent call last): + ... + ValueError: 0.200000000000000 + 0.300000000000000*I is not a valid boundary point in the UHP model + + TESTS: + + In the PD model, the coordinates of a point are in the unit disk + in the complex plane `\CC`:: + + sage: HyperbolicPlane().PD().get_point(0) + Point in PD 0 + sage: HyperbolicPlane().PD().get_point(1) + Boundary point in PD 1 + + In the KM model, the coordinates of a point are in the unit disk + in the real plane `\RR^2`:: + + sage: HyperbolicPlane().KM().get_point((0,0)) + Point in KM (0, 0) + sage: HyperbolicPlane().KM().get_point((1,0)) + Boundary point in KM (1, 0) + + In the HM model, the coordinates of a poi nt are on the + hyperboloidgiven by `x^2 + y^2 - z^2 = -1`:: + + sage: HyperbolicPlane().HM().get_point((0,0,1)) + Point in HM (0, 0, 1) + sage: HyperbolicPlane().HM().get_point((1,0,0), is_boundary=True) + Traceback (most recent call last): + ... + NotImplementedError: boundary points are not implemented in the HM model + """ + def __init__(self, model, coordinates, is_boundary, check=True, **graphics_options): + r""" + See ``HyperbolicPoint`` for full documentation. + + EXAMPLES:: + + sage: p = HyperbolicPlane().UHP().get_point(I) + sage: TestSuite(p).run() + """ + if is_boundary: + if not model.is_bounded(): + raise NotImplementedError("boundary points are not implemented in the {0} model".format(model.short_name())) + if check and not model.boundary_point_in_model(coordinates): + raise ValueError( + "{0} is not a valid".format(coordinates) + + " boundary point in the {0} model".format(model.short_name())) + elif check and not model.point_in_model(coordinates): + raise ValueError( + "{0} is not a valid".format(coordinates) + + " point in the {0} model".format(model.short_name())) + + if isinstance(coordinates, tuple): + coordinates = vector(coordinates) + self._coordinates = coordinates + self._bdry = is_boundary + self._graphics_options = graphics_options + + Element.__init__(self, model) + + ##################### + # "Private" Methods # + ##################### + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLES:: + + sage: HyperbolicPlane().UHP().get_point(3 + 4*I) + Point in UHP 4*I + 3 + + sage: HyperbolicPlane().PD().get_point(1/2 + I/2) + Point in PD 1/2*I + 1/2 + + sage: HyperbolicPlane().KM().get_point((1/2, 1/2)) + Point in KM (1/2, 1/2) + + sage: HyperbolicPlane().HM().get_point((0,0,1)) + Point in HM (0, 0, 1) + + sage: HyperbolicPlane().UHP().get_point(infinity) + Boundary point in UHP +Infinity + + sage: HyperbolicPlane().PD().get_point(-1) + Boundary point in PD -1 + + sage: HyperbolicPlane().KM().get_point((0, -1)) + Boundary point in KM (0, -1) + """ + if self._bdry: + base = "Boundary point" + else: + base = "Point" + return base + " in {0} {1}".format(self.parent().short_name(), self._coordinates) + + def _latex_(self): + r""" + EXAMPLES:: + + sage: from sage.geometry.hyperbolic_space.hyperbolic_point import * + sage: p = HyperbolicPlane().UHP().get_point(0) + sage: latex(p) + 0 + sage: q = HyperbolicPlane().HM().get_point((0,0,1)) + sage: latex(q) + \left(0,\,0,\,1\right) + """ + return latex(self._coordinates) + + def __eq__(self, other): + r""" + Return ``True`` if ``self`` is equal to ``other``. + + EXAMPLES:: + + sage: from sage.geometry.hyperbolic_space.hyperbolic_point import * + sage: p1 = HyperbolicPlane().UHP().get_point(1 + I) + sage: p2 = HyperbolicPlane().UHP().get_point(2 + I) + sage: p1 == p2 + False + sage: p1 == p1 + True + + :: + + sage: p1 = HyperbolicPlane().PD().get_point(0) + sage: p2 = HyperbolicPlane().PD().get_point(1/2 + 2*I/3) + sage: p1 == p2 + False + sage: p1 == p1 + True + + :: + + sage: p1 = HyperbolicPlane().KM().get_point((0,0)) + sage: p2 = HyperbolicPlane().KM().get_point((0, 1/2)) + sage: p1 == p2 + False + + :: + + sage: p1 = HyperbolicPlane().HM().get_point((0,0,1)) + sage: p2 = HyperbolicPlane().HM().get_point((0,0,1/1)) + sage: p1 == p2 + True + """ + return (isinstance(other, HyperbolicPoint) + and self.parent() is other.parent() + and bool(self._coordinates == other._coordinates)) + + def __rmul__(self, other): + r""" + Implement the action of matrices on points of hyperbolic space. + + EXAMPLES:: + + sage: A = matrix(2, [0, 1, 1, 0]) + sage: A = HyperbolicPlane().UHP().get_isometry(A) + sage: A * HyperbolicPlane().UHP().get_point(2 + I) + Point in UHP 1/5*I + 2/5 + + We also lift matrices into isometries:: + + sage: B = diagonal_matrix([-1, -1, 1]) + sage: B = HyperbolicPlane().HM().get_isometry(B) + sage: B * HyperbolicPlane().HM().get_point((0, 1, sqrt(2))) + Point in HM (0, -1, sqrt(2)) + """ + if isinstance(other, HyperbolicIsometry): + return other(self) + elif is_Matrix(other): + # TODO: Currently the __mul__ from the matrices gets called first + # and returns an error instead of calling this method + A = self.parent().get_isometry(other) + return A(self) + else: + raise TypeError("unsupported operand type(s) for *:" + "{0} and {1}".format(self, other)) + + ####################### + # Setters and Getters # + ####################### + + def coordinates(self): + r""" + Return the coordinates of the point. + + EXAMPLES:: + + sage: HyperbolicPlane().UHP().get_point(2 + I).coordinates() + I + 2 + + sage: HyperbolicPlane().PD().get_point(1/2 + 1/2*I).coordinates() + 1/2*I + 1/2 + + sage: HyperbolicPlane().KM().get_point((1/3, 1/4)).coordinates() + (1/3, 1/4) + + sage: HyperbolicPlane().HM().get_point((0,0,1)).coordinates() + (0, 0, 1) + """ + return self._coordinates + + def model(self): + r""" + Return the model to which the :class:`HyperbolicPoint` belongs. + + EXAMPLES:: + + sage: HyperbolicPlane().UHP().get_point(I).model() + Hyperbolic plane in the Upper Half Plane Model model + + sage: HyperbolicPlane().PD().get_point(0).model() + Hyperbolic plane in the Poincare Disk Model model + + sage: HyperbolicPlane().KM().get_point((0,0)).model() + Hyperbolic plane in the Klein Disk Model model + + sage: HyperbolicPlane().HM().get_point((0,0,1)).model() + Hyperbolic plane in the Hyperboloid Model model + """ + return self.parent() + + def to_model(self, model): + """ + Convert ``self`` to the ``model``. + + INPUT: + + - ``other`` -- (a string representing) the image model + + EXAMPLES:: + + sage: UHP = HyperbolicPlane().UHP() + sage: PD = HyperbolicPlane().PD() + sage: PD.get_point(1/2+I/2).to_model(UHP) + Point in UHP I + 2 + sage: PD.get_point(1/2+I/2).to_model('UHP') + Point in UHP I + 2 + """ + if isinstance(model, str): + model = getattr(self.parent().realization_of(), model)() + return model(self) + + def is_boundary(self): + """ + Return ``True`` if ``self`` is a boundary point. + + EXAMPLES:: + + sage: PD = HyperbolicPlane().PD() + sage: p = PD.get_point(0.5+.2*I) + sage: p.is_boundary() + False + sage: p = PD.get_point(I) + sage: p.is_boundary() + True + """ + return self._bdry + + def update_graphics(self, update=False, **options): + r""" + Update the graphics options of a :class:`HyperbolicPoint`. + If ``update`` is ``True``, update rather than overwrite. + + EXAMPLES:: + + sage: p = HyperbolicPlane().UHP().get_point(I); p.graphics_options() + {} + + sage: p.update_graphics(color = "red"); p.graphics_options() + {'color': 'red'} + + sage: p.update_graphics(color = "blue"); p.graphics_options() + {'color': 'blue'} + + sage: p.update_graphics(True, size = 20); p.graphics_options() + {'color': 'blue', 'size': 20} + """ + if not update: + self._graphics_options = {} + self._graphics_options.update(**options) + + def graphics_options(self): + r""" + Return the graphics options of the current point. + + EXAMPLES:: + + sage: p = HyperbolicPlane().UHP().get_point(2 + I, color="red") + sage: p.graphics_options() + {'color': 'red'} + """ + return self._graphics_options + + def symmetry_involution(self): + r""" + Return the involutory isometry fixing the given point. + + EXAMPLES:: + + sage: z = HyperbolicPlane().UHP().get_point(3 + 2*I) + sage: z.symmetry_involution() + Isometry in UHP + [ 3/2 -13/2] + [ 1/2 -3/2] + + sage: HyperbolicPlane().UHP().get_point(I).symmetry_involution() + Isometry in UHP + [ 0 -1] + [ 1 0] + + sage: HyperbolicPlane().PD().get_point(0).symmetry_involution() + Isometry in PD + [-I 0] + [ 0 I] + + sage: HyperbolicPlane().KM().get_point((0, 0)).symmetry_involution() + Isometry in KM + [-1 0 0] + [ 0 -1 0] + [ 0 0 1] + + sage: HyperbolicPlane().HM().get_point((0,0,1)).symmetry_involution() + Isometry in HM + [-1 0 0] + [ 0 -1 0] + [ 0 0 1] + + sage: p = HyperbolicPlane().UHP().random_element() + sage: A = p.symmetry_involution() + sage: A*p == p + True + + sage: A.preserves_orientation() + True + + sage: A*A == HyperbolicPlane().UHP().get_isometry(identity_matrix(2)) + True + """ + R = self.parent().realization_of().a_realization() + A = R(self).symmetry_involution() + return self.parent().get_isometry(A) + + ########### + # Display # + ########### + + def show(self, boundary=True, **options): + r""" + Plot ``self``. + + EXAMPLES:: + + sage: HyperbolicPlane().PD().get_point(0).show() + Graphics object consisting of 2 graphics primitives + sage: HyperbolicPlane().KM().get_point((0,0)).show() + Graphics object consisting of 2 graphics primitives + sage: HyperbolicPlane().HM().get_point((0,0,1)).show() + Graphics3d Object + """ + p = self.coordinates() + if p == infinity: + raise NotImplementedError("can't draw the point infinity") + + opts = {'axes': False, 'aspect_ratio': 1} + opts.update(self.graphics_options()) + opts.update(options) + + from sage.plot.point import point + from sage.misc.functional import numerical_approx + + if self._bdry: # It is a boundary point + p = numerical_approx(p) + pic = point((p, 0), **opts) + if boundary: + bd_pic = self._model.get_background_graphic(bd_min=p - 1, + bd_max=p + 1) + pic = bd_pic + pic + else: # It is an interior point + if p in RR: + p = CC(p) + elif hasattr(p, 'iteritems') or hasattr(p, '__iter__'): + p = [numerical_approx(k) for k in p] + else: + p = numerical_approx(p) + pic = point(p, **opts) + if boundary: + bd_pic = self.parent().get_background_graphic() + pic = bd_pic + pic + return pic + +class HyperbolicPointUHP(HyperbolicPoint): + r""" + A point in the UHP model. + + INPUT: + + - the coordinates of a point in the unit disk in the complex plane `\CC` + + EXAMPLES:: + + sage: HyperbolicPlane().UHP().get_point(2*I) + Point in UHP 2*I + + sage: HyperbolicPlane().UHP().get_point(1) + Boundary point in UHP 1 + """ + def symmetry_involution(self): + r""" + Return the involutory isometry fixing the given point. + + EXAMPLES:: + + sage: HyperbolicPlane().UHP().get_point(3 + 2*I).symmetry_involution() + Isometry in UHP + [ 3/2 -13/2] + [ 1/2 -3/2] + """ + p = self._coordinates + x, y = real(p), imag(p) + if y > 0: + M = matrix([[x/y, -(x**2/y) - y], [1/y, -(x/y)]]) + return self.parent().get_isometry(M) + raise ValueError("cannot determine the isometry of a boundary point") + + def show(self, boundary=True, **options): + r""" + Plot ``self``. + + EXAMPLES:: + + sage: HyperbolicPlane().UHP().get_point(I).show() + Graphics object consisting of 2 graphics primitives + sage: HyperbolicPlane().UHP().get_point(0).show() + Graphics object consisting of 2 graphics primitives + sage: HyperbolicPlane().UHP().get_point(infinity).show() + Traceback (most recent call last): + ... + NotImplementedError: can't draw the point infinity + """ + p = self.coordinates() + if p == infinity: + raise NotImplementedError("can't draw the point infinity") + opts = {'axes': False, 'aspect_ratio': 1} + opts.update(self.graphics_options()) + opts.update(options) + from sage.misc.functional import numerical_approx + p = numerical_approx(p + 0 * I) + from sage.plot.point import point + if self._bdry: + pic = point((p, 0), **opts) + if boundary: + bd_pic = self.parent().get_background_graphic(bd_min=p - 1, + bd_max=p + 1) + pic = bd_pic + pic + else: + pic = point(p, **opts) + if boundary: + cent = real(p) + bd_pic = self.parent().get_background_graphic(bd_min=cent - 1, + bd_max=cent + 1) + pic = bd_pic + pic + return pic diff --git a/src/sage/graphs/all.py b/src/sage/graphs/all.py index 57fd01e8175..6a01362e303 100644 --- a/src/sage/graphs/all.py +++ b/src/sage/graphs/all.py @@ -17,7 +17,6 @@ import sage.graphs.digraph lazy_import("sage.graphs", "graph_coloring") import sage.graphs.graph_decompositions -import sage.graphs.modular_decomposition.modular_decomposition import sage.graphs.comparability from sage.graphs.cliquer import * from graph_database import graph_db_info diff --git a/src/sage/graphs/generic_graph.py b/src/sage/graphs/generic_graph.py index ef9cbc7e562..63158f74ff5 100644 --- a/src/sage/graphs/generic_graph.py +++ b/src/sage/graphs/generic_graph.py @@ -7978,8 +7978,18 @@ def dominating_set(self, independent=False, total=False, value_only=False, solve sage: G = graphs.PetersenGraph() sage: G.dominating_set(total=True,value_only=True) 4 + + The dominating set is calculated for both the directed and undirected + graphs (modification introduced in :trac:`17905`):: + + sage: g=digraphs.Path(3) + sage: g.dominating_set(value_only=True) + 2 + sage: g=graphs.PathGraph(3) + sage: g.dominating_set(value_only=True) + 1 + """ - self._scream_if_not_simple(allow_multiple_edges=True, allow_loops=not total) from sage.numerical.mip import MixedIntegerLinearProgram @@ -7988,10 +7998,13 @@ def dominating_set(self, independent=False, total=False, value_only=False, solve b=p.new_variable(binary=True) # For any vertex v, one of its neighbors or v itself is in - # the minimum dominating set - for v in g.vertices(): - p.add_constraint(int(not total)*b[v]+p.sum([b[u] for u in g.neighbors(v)]),min=1) + # the minimum dominating set. If g is directed, we use the + # in neighbors of v instead. + neighbors_iter=g.neighbor_in_iterator if g.is_directed() else g.neighbor_iterator + + for v in g.vertices(): + p.add_constraint(int(not total)*b[v]+p.sum([b[u] for u in neighbors_iter(v)]),min=1) if independent: # no two adjacent vertices are in the set diff --git a/src/sage/graphs/graph.py b/src/sage/graphs/graph.py index d4c07f0bca1..726e3414e79 100644 --- a/src/sage/graphs/graph.py +++ b/src/sage/graphs/graph.py @@ -6355,6 +6355,12 @@ def modular_decomposition(self): r""" Returns the modular decomposition of the current graph. + .. NOTE:: + + In order to use this method you must install the + ``modular_decomposition`` optional package. See + :mod:`sage.misc.package`. + Crash course on modular decomposition: A module `M` of a graph `G` is a proper subset of its vertices @@ -6419,12 +6425,12 @@ def modular_decomposition(self): The Bull Graph is prime:: - sage: graphs.BullGraph().modular_decomposition() + sage: graphs.BullGraph().modular_decomposition() # optional -- modular_decomposition ('Prime', [3, 4, 0, 1, 2]) The Petersen Graph too:: - sage: graphs.PetersenGraph().modular_decomposition() + sage: graphs.PetersenGraph().modular_decomposition() # optional -- modular_decomposition ('Prime', [2, 6, 3, 9, 7, 8, 0, 1, 5, 4]) This a clique on 5 vertices with 2 pendant edges, though, has a more @@ -6433,7 +6439,7 @@ def modular_decomposition(self): sage: g = graphs.CompleteGraph(5) sage: g.add_edge(0,5) sage: g.add_edge(0,6) - sage: g.modular_decomposition() + sage: g.modular_decomposition() # optional -- modular_decomposition ('Serie', [0, ('Parallel', [5, ('Serie', [1, 4, 3, 2]), 6])]) ALGORITHM: @@ -6469,12 +6475,16 @@ def modular_decomposition(self): vol 4, number 1, pages 41--59, 2010 http://www.lirmm.fr/~paul/md-survey.pdf """ + try: + from sage.graphs.modular_decomposition import modular_decomposition + except ImportError: + raise RuntimeError("In order to use this method you must " + "install the modular_decomposition package") + self._scream_if_not_simple() from sage.misc.stopgap import stopgap stopgap("Graph.modular_decomposition is known to return wrong results",13744) - from sage.graphs.modular_decomposition.modular_decomposition import modular_decomposition - D = modular_decomposition(self) id_label = dict(enumerate(self.vertices())) @@ -6485,22 +6495,29 @@ def modular_decomposition(self): def is_prime(self): r""" - Tests whether the current graph is prime. A graph is prime if - all its modules are trivial (i.e. empty, all of the graph or - singletons)-- see ``self.modular_decomposition?``. + Tests whether the current graph is prime. + + A graph is prime if all its modules are trivial (i.e. empty, all of the + graph or singletons) -- see :meth:`modular_decomposition`. + + .. NOTE:: + + In order to use this method you must install the + ``modular_decomposition`` optional package. See + :mod:`sage.misc.package`. EXAMPLE: The Petersen Graph and the Bull Graph are both prime:: - sage: graphs.PetersenGraph().is_prime() + sage: graphs.PetersenGraph().is_prime() # optional - modular_decomposition True - sage: graphs.BullGraph().is_prime() + sage: graphs.BullGraph().is_prime() # optional - modular_decomposition True Though quite obviously, the disjoint union of them is not:: - sage: (graphs.PetersenGraph() + graphs.BullGraph()).is_prime() + sage: (graphs.PetersenGraph() + graphs.BullGraph()).is_prime() # optional - modular_decomposition False """ diff --git a/src/sage/graphs/modular_decomposition/modular_decomposition.pxd b/src/sage/graphs/modular_decomposition.pxd similarity index 94% rename from src/sage/graphs/modular_decomposition/modular_decomposition.pxd rename to src/sage/graphs/modular_decomposition.pxd index 6000265dedf..ac6186e8469 100644 --- a/src/sage/graphs/modular_decomposition/modular_decomposition.pxd +++ b/src/sage/graphs/modular_decomposition.pxd @@ -2,9 +2,7 @@ include "sage/ext/interrupt.pxi" include 'sage/ext/cdefs.pxi' include 'sage/ext/stdsage.pxi' - - -cdef extern from "src/dm_english.h": +cdef extern from "modular_decomposition.h": ctypedef struct noeud: pass @@ -40,6 +38,4 @@ cdef extern from "src/dm_english.h": int n c_adj ** G -cdef extern from "src/dm_english.h": - c_noeud *decomposition_modulaire(c_graphe G) diff --git a/src/sage/graphs/modular_decomposition/modular_decomposition.pyx b/src/sage/graphs/modular_decomposition.pyx similarity index 91% rename from src/sage/graphs/modular_decomposition/modular_decomposition.pyx rename to src/sage/graphs/modular_decomposition.pyx index 855be3867de..89d8c2b9718 100644 --- a/src/sage/graphs/modular_decomposition/modular_decomposition.pyx +++ b/src/sage/graphs/modular_decomposition.pyx @@ -69,13 +69,13 @@ cpdef modular_decomposition(g): The Bull Graph is prime:: - sage: from sage.graphs.modular_decomposition.modular_decomposition import modular_decomposition - sage: modular_decomposition(graphs.BullGraph()) + sage: from sage.graphs.modular_decomposition import modular_decomposition # optional -- modular_decomposition + sage: modular_decomposition(graphs.BullGraph()) # optional -- modular_decomposition ('Prime', [3, 4, 0, 1, 2]) The Petersen Graph too:: - sage: modular_decomposition(graphs.PetersenGraph()) + sage: modular_decomposition(graphs.PetersenGraph()) # optional -- modular_decomposition ('Prime', [2, 6, 3, 9, 7, 8, 0, 1, 5, 4]) This a clique on 5 vertices with 2 pendant edges, though, has a more @@ -84,7 +84,7 @@ cpdef modular_decomposition(g): sage: g = graphs.CompleteGraph(5) sage: g.add_edge(0,5) sage: g.add_edge(0,6) - sage: modular_decomposition(g) + sage: modular_decomposition(g) # optional -- modular_decomposition ('Serie', [0, ('Parallel', [5, ('Serie', [1, 4, 3, 2]), 6])]) @@ -136,7 +136,6 @@ cpdef modular_decomposition(g): a.suiv = G.G[j] G.G[j] = a - R = decomposition_modulaire(G) return build_dict_from_decomposition(R) diff --git a/src/sage/graphs/modular_decomposition/__init__.py b/src/sage/graphs/modular_decomposition/__init__.py deleted file mode 100644 index ff67b47096b..00000000000 --- a/src/sage/graphs/modular_decomposition/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# This file is not empty ! - -import sage.graphs.modular_decomposition.modular_decomposition - diff --git a/src/sage/graphs/modular_decomposition/src/dm.c b/src/sage/graphs/modular_decomposition/src/dm.c deleted file mode 100644 index 4eca712c9a6..00000000000 --- a/src/sage/graphs/modular_decomposition/src/dm.c +++ /dev/null @@ -1,1286 +0,0 @@ -/****************************************************** - -Copyright 2004, 2010 Fabien de Montgolfier -fm@liafa.jussieu.fr - -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. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -**********************************************************/ - -/******************************************************** - - DECOMPOSITION MODULAIRE DE GRAPHES NON-ORIENTES - -Cet algorithme construit l'arbre de decomposition modulaire -d'un graphe donne sous forme d'une matrice d'adjacence. -Il s'effectue en temps O(m log n) temps et $O(m)$ espace. -Il est la concatenation de deux algorithmes distincts. -Le premier realise une permutation factorisante des sommets du graphe -(pour cette notion, cf these de Christian Capelle) -grace a une technique d'affinage de partitions (cf Habib, Paul & Viennot) -Le second construit l'arbre de decomposition modulaire a partir de cette -permutation, cf mon memoire de DEA -Montpellier, decembre 2000 -********************************************************/ - -#include "dm_english.h" -#include -#include - -#define DEBUG 0 /* si 0 aucune sortie graphique!! */ - -/* dm.h definit les constantes FEUILLE, MODULE, etc... -ainsi que les structures noeud et fils. Les autres -structures n'ont pas a etre connues par les programmes -exterieurs et sont donc definies ici. */ - - -/* un sommet du graphe -(utilise dans la premiere partie seulement, ainsi que ce qui suit)*/ -typedef struct Sommet { - int place;/* numero du sommet dans la NOUVELLE numerotation */ - int nom; /* numero du sommet dans l'ANCIENNE numerotation */ - /* On a donc sigma(nom)=place */ - struct Sadj *adj; - struct SClasse *classe; -} sommet; - -/* liste d'adjacence d'un sommet, DOUBLEMENT chainee*/ -typedef struct Sadj { - struct Sommet *pointe; - struct Sadj *suiv; - struct Sadj *prec; -} sadj; - -/* classe de la partition courante, - organisees en liste chainnee selon l'ordre de la partition */ -typedef struct SClasse { - int debut; - int fin; - struct Sommet *firstpivot; - int inpivot; /*indice de la classe dans le tableau pivot */ - int inmodule; /* (resp module); -1 si non present */ - int whereXa; /* lie le couple X/Xa: vaut - 0 si X n'est actuellement lie a aucun Xa - -1 si Xa est a gauche - +1 si Xa est a droite */ - struct SClasse *suiv; /* forment une liste chainee */ - struct SClasse *prec; /*...doublement */ -} sclasse; - -/* plein de parametres statiques que algo1() donne a Raffine() */ -typedef struct Info { - sclasse **pivot; - int *ipivot; - sclasse **module; - int *imodule; - int *numclasse; - int *n; -} info; - -/* clef a deux entrees utilisee pour le tri lineaire - represente l'arrete ij */ -typedef struct Clef2{ - int i; //sommet pointeur - int nom; // nom du sommet pointe - int place; //place du sommet pointe -} clef2; - -/************************************************************* -utilitaires -*************************************************************/ -void *fabmalloc(size_t s) -/* malloc sans erreur */ -{ - void *p; - p=malloc(s); - if(p==NULL) - { - perror("Erreur de malloc!\n"); - exit(1); - } - return p; -} - -int min(int a, int b) -{ - return (a b) ? a : b; -} -/************************************************************** -Premiere passe de l'algorithme: il s'agit de trouver une -permutation factorisante du graphe. Nous utilisons les -techniques de raffinement de partition. Tout cela est -explique dans l'article de Habib, Viennot & Paul, dont je ne -fais ici que transcrire le travail. -****************************************************************/ - -void printS(sommet ** S, int n) -{ - /* imprimme S selon S et selon les classes */ - int i; - sclasse *s; - - for (s = S[0]->classe; s != NULL; s = s->suiv) { - printf("[ "); - for (i = s->debut; i <= s->fin; i++) - printf("%i ", 1 + S[i]->nom); - printf("] "); - } - printf("\n"); -} - -sclasse *nouvclasse(sclasse * un, sclasse * deux) -{ - /* cree une nouvelle classe et l'insere entre un et deux; - on suppose que si un et deux sont pas NULL alors - FORCEMENT un=deux->suiv */ - - sclasse *nouv; - nouv = (sclasse *) fabmalloc(sizeof(sclasse)); - nouv->whereXa = 0; - nouv->inpivot = -1; - nouv->inmodule = -1; - nouv->firstpivot = NULL; - nouv->prec = un; - if (un != NULL) /* accroche pas en tete de chaine */ - un->suiv = nouv; - nouv->suiv = deux; - if (deux != NULL) /* pas en queue de chaine */ - deux->prec = nouv; - - /* debut et fin ne sont PAS initialises! */ - return nouv; -} - -void permute(sommet ** S, int a, int b) -{ - /* transpose les sommets a et b dans S */ - /* ne touche pas aux classes! */ - sommet *tmp; - S[a]->place = b; - S[b]->place = a; - tmp = S[a]; - S[a] = S[b]; - S[b] = tmp; -} - -void Raffiner(sommet ** S, sommet * p, sommet * centre, info * I) -{ - /* melange raffiner, pivotset, insertright et addpivot */ - sadj *a; /* parcours l'adjacence du pivot */ - sommet *x; /* sommet quiva changer de classe */ - sclasse *X, *Xa; /* x in X; Xa nouv classe de x */ - sclasse *Z; - sclasse **pivot; - sclasse **module; - int *ipivot, *imodule, *numclasse, n; - - if (DEBUG) - printf("Raffinage avec le pivot %i\n", 1 + p->nom); - pivot = I->pivot; - module = I->module; - ipivot = I->ipivot; - imodule = I->imodule; - numclasse = I->numclasse; - n = *(I->n); - - for (a = p->adj; a != NULL; a = a->suiv) { - x = a->pointe; - X = x->classe; - if (X == p->classe) - continue; /* on raffine pas la classe du pivot! */ - - if (X->whereXa == 0) { - /* c'est la premiere fois qu'on trouve un x - appartenant a X lors de cet appel a raffiner */ - - if ((centre->place < x->place && x->place < p->place) - || (p->place < x->place && x->place < centre->place)) { - /* insere a gauche */ - Xa = nouvclasse(X->prec, X); - (*numclasse)++; - permute(S, x->place, X->debut); - X->debut++; - X->whereXa = -1; - Xa->whereXa = 1; /* besoin dans le second tour */ - } - else { /* insere a droite */ - - Xa = nouvclasse(X, X->suiv); - (*numclasse)++; - permute(S, x->place, X->fin); - X->fin--; - X->whereXa = 1; - Xa->whereXa = -1; - } - x->classe = Xa; - Xa->debut = x->place; - Xa->fin = x->place; - } - else { - if (X->whereXa == -1) { - Xa = X->prec; - permute(S, x->place, X->debut); - X->debut++; - Xa->fin++; - } - else { - Xa = X->suiv; - permute(S, x->place, X->fin); - X->fin--; - Xa->debut--; - } - x->classe = Xa; - } - } - - for (a = p->adj; a != NULL; a = a->suiv) - /* deuxieme couche! Maintenant on va faire les addpivot, - et remettre les whereXa a 0 - Noter qu'on lit les Xa et plus les X */ - { - x = a->pointe; - Xa = x->classe; - if (Xa->whereXa == 0) - continue; /* deja remis a zero! */ - if (Xa->whereXa == -1) - X = Xa->prec; - else - X = Xa->suiv; - - if (X->debut > X->fin) { - /*on a trop enleve! X est vide - -> on va le supprimer mechamment */ - - (*numclasse)--; - if (Xa->whereXa == 1) { /*deconnecte */ - Xa->suiv = X->suiv; - if (Xa->suiv != NULL) - Xa->suiv->prec = Xa; - } - else { - Xa->prec = X->prec; - if (Xa->prec != NULL) - Xa->prec->suiv = Xa; - } - Xa->inpivot = X->inpivot; - if (X->inpivot != -1) /* ecrase X dans pivot */ - pivot[X->inpivot] = Xa; - Xa->inmodule = X->inmodule; - if (X->inmodule != -1) /* ecrase X dans pivot */ - module[X->inmodule] = Xa; - - Xa->whereXa = 0; - continue; - } - - /* Maintenant on fait addpivot(X,Xa) - noter que X et Xa sont non vides */ - - if (X->inpivot == -1) { - if ((X->inmodule != -1) - && (X->fin - X->debut < Xa->fin - Xa->debut)) { - /* remplace X par Xa dans module */ - module[X->inmodule] = Xa; - Xa->inmodule = X->inmodule; - X->inmodule = -1; - if (DEBUG) - printf("Dans module %i-%i ecrase %i-%i\n", - 1 + S[Xa->debut]->nom, 1 + S[Xa->fin]->nom, - 1 + S[X->debut]->nom, 1 + S[X->fin]->nom); - } - else { - if (X->inmodule == -1) { - if (X->fin - X->debut < Xa->fin - Xa->debut) - Z = Xa; - else - Z = X; - /* ajoute Z (=max(X,Xa)) a module */ - module[(*imodule)] = Z; - Z->inmodule = (*imodule); - (*imodule)++; - if (DEBUG) - printf("module empile:%i-%i\n", - 1 + S[Z->debut]->nom, 1 + S[Z->fin]->nom); - } - } - } - - if (X->inpivot != -1) - Z = Xa; - else if (X->fin - X->debut < Xa->fin - Xa->debut) - Z = X; - else - Z = Xa; - /* on empile Z dans pivot */ - pivot[(*ipivot)] = Z; - Z->inpivot = (*ipivot); - (*ipivot)++; - if (DEBUG) - printf("pivot empile: %i-%i\n", 1 + S[Z->debut]->nom, - 1 + S[Z->fin]->nom); - X->whereXa = 0; - Xa->whereXa = 0; - } - if (DEBUG) { - printS(S, n); - printf("\n"); - } -} - -sommet **algo1(graphe G) - /* Entree: un graphe G - Sortie: une permutation factorisante de G, - donnee sous la forme d'un tableau de structures Sommet ordonnees selon sigma. - d'apres le travail de Habib/Paul/Viennot */ -{ - int n; // nombre de sommets de G - - sclasse **pivot; /*pile des pivots */ - int ipivot = 0; /*indice sur la precedante */ - - sclasse **module; /*idem, modules */ - int imodule = 0; - - sclasse *singclasse; - /*invariant: toute classe avant singclasse dans la chaine */ - /*a un seul element */ - int numclasse; /* quand vaut n, on a fini!! */ - - sclasse *C1; /*premiere classe, tete de la chaine */ - sclasse *Y; /*classe qui raffine */ - sclasse *X; /*classe raffinee */ - sclasse *Xa, *Xc; /* morceaux de X */ - sommet *x; /* x in X */ - sommet *y; /* y in Y */ - sommet *centre; /* le centre du raffinage actuel */ - - sommet **S; /*la permutation factorisante ! */ - - int i, j; /*divers indices */ - sommet *scourant; /* pour l'init */ - sadj *nextadj; /*sommet adjacent suivant */ - adj *nextadj2; /* idem mais de type adj */ - info Inf; /* diverses info a passer a raffiner */ - - /* debut des initialisations */ - n=G.n; - /*initialisation des tableaux */ - module = (sclasse **) fabmalloc(n * sizeof(sclasse *)); - pivot = (sclasse **) fabmalloc(n * sizeof(sclasse *)); - S = (sommet **) fabmalloc(n * sizeof(sommet *)); - /* on va initialiser la permutation factorisante, - ainsi que chaque structure sommet */ - C1 = nouvclasse(NULL, NULL); - numclasse = 1; - singclasse = C1; - C1->debut = 0; - C1->fin = n - 1; - for (i = 0; i < n; i++) { - /* initialisation des sommets */ - /* notre bebe est le sommet i dans M */ - scourant = (sommet *) fabmalloc(sizeof(struct Sommet)); - scourant->nom = i; - scourant->place = i; /* a ce point S=identite */ - scourant->adj = NULL; /* pas encore d'adjacence */ - scourant->classe = C1; - S[i] = scourant; - } - for (i = 0; i < n; i++) - { - nextadj2 = G.G[i]; - while(nextadj2 != NULL) - { - j=nextadj2->s; //numero du sommet pointe - if((j<0)||(j>=n)) - { - perror("Graphe invalide (numero de sommet erronne)!\n"); - exit(1); - } - nextadj = (sadj *) fabmalloc(sizeof(struct Sadj)); - //un nouveau sadj - nextadj->pointe = S[j]; - nextadj->suiv = S[i]->adj; //tete de liste - if(nextadj->suiv!=NULL) - nextadj->suiv->prec=nextadj; - nextadj->prec=NULL; - S[i]->adj = nextadj; /*et le tour est joue */ - nextadj2=nextadj2->suiv; - } - } - /* NB: module et pivot sont vides */ - Inf.pivot = pivot; - Inf.ipivot = &ipivot; - Inf.module = module; - Inf.imodule = &imodule; - Inf.numclasse = &numclasse; - Inf.n = &n; - /* init terminnee */ - - while (1) { - while (ipivot > 0 || imodule > 0) { - while (ipivot > 0) { - /*cette boucle raffine selon tous les sommets - de la premiere classe dans pivot */ - - Y = pivot[ipivot - 1]; - ipivot--; - Y->inpivot = -1; - - for (i = Y->debut; i <= Y->fin; i++) - Raffiner(S, S[i], centre, &Inf); - - /* une optimisation de la fin de l'algo */ - if (numclasse == n) - return (S); - } - /*maintenant pivot est vide, mais peut-etre pas module */ - if (imodule > 0) { - /* relance par un sommet (pas au pif...) */ - /* de chaque module qui le represente */ - Y = module[imodule - 1]; - imodule--; - Y->inmodule = -1; - y = S[Y->debut]; /* le firstpivot sera toujours... */ - Y->firstpivot = y; /* le premier!! */ - if (DEBUG) - printf("module-pivot %i-%i: sommet %i\n", - 1 + S[Y->debut]->nom, 1 + S[Y->fin]->nom, - 1 + y->nom); - Raffiner(S, y, centre, &Inf); - } - } - /* a ce point, pivot et module sont vides... - pas de pb! On va faire initpartition HERE */ - if (DEBUG) - printf("\nInit Partition\n"); - /**** ajoute ici pour debbugger, mais moche!! */ - singclasse = S[0]->classe; - while ((singclasse != NULL) && - (singclasse->debut == singclasse->fin)) - { - singclasse = singclasse->suiv; - } - /* singclasse est la premiere classe - non singlette, sauf si: */ - if (singclasse == NULL) - /* on a n classes singlettes? ben c'est gagne! */ - { - return (S); - } - if (singclasse == NULL && numclasse < n) { - perror("c'est pas normal! Ca termine trop vite!\n"); - exit(1); - } - - X = singclasse; - x = X->firstpivot; - if (x == NULL) - x = S[X->debut]; - else /* remet firstpivot a NULL!! */ - X->firstpivot = NULL; - - if (DEBUG) - printf("Relance dans le module %i-%i avec le sommet %i\n", - 1 + S[X->debut]->nom, 1 + S[X->fin]->nom, 1 + x->nom); - - centre = x; /*important! */ - /* astuce: on place {x} en tete de X - ensuite, on raffine S selon x -> seule X est coupee - il y a alors {x} X Xa - -> on met {x} en queue de X et c'est bon! - ainsi on a bien nonvoisins-x-voisons */ - Xc = nouvclasse(X->prec, X); - numclasse++; - x->classe = Xc; - permute(S, x->place, X->debut); - X->debut++; - Xc->debut = x->place; - Xc->fin = x->place; - Raffiner(S, x, x, &Inf); - /* X existe-il encore? */ - if (X->debut > X->fin) - continue; - /* echange de x et {x}. Init: -{x}-X- */ - Xc->suiv = X->suiv; - if (X->suiv != NULL) - X->suiv->prec = Xc; - X->prec = Xc->prec; - if (Xc->prec != NULL) - Xc->prec->suiv = X; - X->suiv = Xc; - Xc->prec = X; - permute(S, x->place, X->fin); - Xc->debut = x->place; - Xc->fin = x->place; - X->debut--; - X->fin--; - //antibug? - singclasse=X; - /* now -X-{x}- */ - if (DEBUG) - printS(S, n); - } -} - -/*************************************************************** -Etape intermediaire: trier toutes les listes d'adjacence -selon S. ce sont les listes de type sadj qui sont concernees -***************************************************************/ -int Calculm(graphe G) -/* compte le nombre d'arretes du graphe */ -{ - int i,r; adj *a; - r=0; - for(i=0;isuiv; - r++; - } - } - if(r%2!=0) - { - perror("Erreur: nombre impaire d'arrete, graphe non-oriente??\n"); - exit(1); - } - return r/2; // G symetrique! -} - -void TrierTous(sommet **S, int n, int m) -/* trie chaque liste d'adjacence de S*/ -{ - //n sommets, m arretes - int i; // numero du sommet courant - sadj *a,*atmp;// parcours sa liste d'adjacence - clef2 *c; // enregistrement a trier - int *tab1; clef2 **tab2; //tableaux du tri par seaux - tab1=(int *)fabmalloc(n*sizeof(int)); - tab2=(clef2 **)fabmalloc(m * 2 * sizeof(clef2 *)); - for(i=0; iadj; - while(a!=NULL) - { - tab1[i]++; - a=a->suiv; - } - } - //deuxieme passe: frequences cumulees a rebours - // (car les listes d'adjacences se construisent a l'envers - //tab1[n-1]--; // a cause des indices de tableau qui commence a zero - //for(i=n-1;i>0;i--) - // tab1[i-1]+=tab1[i]; - - //deuxieme passe: frequences cumulees - for(i=1;iadj; - while(a!=NULL) - { - /* cree un nouveau record */ - c=(clef2 *)fabmalloc(sizeof(struct Clef2)); - c->i=i; - c->nom=a->pointe->nom; - c->place=a->pointe->place; - /* le place bien dans tab2 */ - tab1[c->place]--; - tab2[tab1[c->place]]=c; - /*et on continue */ - a=a->suiv; - } - } - - //quatrieme passe: detruit les vielles listes d'adjacence - for(i=0; iadj; - while(a!=NULL) - { - atmp=a->suiv; - free(a); - a=atmp; - } - S[i]->adj=NULL; - } - - //derniere passe: reconstruit les listes d'adjacence - for(i=0;i<2*m;i++) - { - c=tab2[i]; - a=(sadj *)fabmalloc(sizeof(struct Sadj)); - a->pointe=S[c->i]; - a->suiv=S[c->place]->adj; //insere en tete - if(a->suiv!=NULL) - a->suiv->prec=a; - a->prec=NULL; - S[c->place]->adj=a; - //nettoie - free(c); - } - free(tab1); - free(tab2); -} - - -/*************************************************************** - Maintenant, la deuxieme partie de l'aglorithme - On va, etant donne la matrice M construite a l'etape precedante, - etablir l'arbre de decomposition modulaire. - Tous les details sont dans mon memoire de DEA -****************************************************************/ -noeud *nouvnoeud(int type, noeud * pere, int sommet, int n) -{ - /* cree un nouveau noeud. Noter que l'on est oblige de passer n - comme parametre car les bords et separateurs droits doivent - etre initilises avec des valeurs >n */ - noeud *nn; - static int compteur = 0; - /*pour donner un ID unique aux noeuds. juste pour debug */ - - nn = (noeud *) fabmalloc(sizeof(noeud)); - nn->type = type; - nn->pere = pere; - /* nn->fpere ne peut etre deja mis a jour... */ - nn->sommet = sommet; - nn->ps = n + 2; - nn->ds = -2; - /*ces valeurs pour distinguer "non calcule" (-2) */ - /* de "abscence de separateur" (-1). De plus, on fera des min et des */ - /* max sur les bords */ - nn->bg = n + 2; - nn->bd = -2; /* idem */ - - nn->fils = NULL; - nn->lastfils = NULL; - nn->id = compteur; - compteur++; - return nn; -} - -void ajoutfils(noeud * pere, noeud * nfils) -{ - fils *nf; - /* noter que c'est un ajout en queue! */ - nf = (fils *) fabmalloc(sizeof(fils)); - nf->pointe = nfils; - nf->suiv = NULL; - if (pere->fils == NULL) - pere->fils = nf; /* on cree le premier fils */ - else - pere->lastfils->suiv = nf; /* on ajoute nf a la chaine */ - pere->lastfils = nf; - nfils->pere = pere; /* normalement: redondant,mais... */ - nfils->fpere = nf; -} - -void fusionne(noeud * pere, noeud * artefact) -{ - /*fusionne un artefact a son pere. - utilise le champ fpere qui permet de savoir ou se greffer - une structure fils sera detruite dans l'operation: artefact->fils */ - fils *greffe; - fils *f; - /* met a jour la liste des peres */ - f = artefact->fils; - while (f != NULL) { - f->pointe->pere = pere; /*avant c'etait ancien... */ - /* f->pointe->fpere est inchange */ - f = f->suiv; - } - /* greffe la liste */ - greffe = artefact->fpere; - artefact->lastfils->suiv = greffe->suiv; - greffe->pointe = artefact->fils->pointe; - greffe->suiv = artefact->fils->suiv; - artefact->fils->pointe->fpere = greffe; /*artefact->fils a disparu */ - if (pere->lastfils == greffe) - pere->lastfils = artefact->lastfils; -} - -void -extraire(noeud * ancien, noeud * nouveau, fils * premier, fils * dernier) -{ - /* extrait la liste [premier...dernier] des fils de l'ancien noeud, - et en fait la liste des fils du nouveau noeud */ - fils *nf; /* il faut une structure fils de plus */ - fils *f; /*indice de mise a jour */ - nf = (fils *) fabmalloc(sizeof(fils)); - nf->pointe = premier->pointe; - nf->suiv = premier->suiv; - premier->pointe->fpere = nf; - nouveau->pere = ancien; - nouveau->fils = nf; - nouveau->lastfils = dernier; - nouveau->bg = premier->pointe->bg; - nouveau->bd = dernier->pointe->bd; - nouveau->ps = premier->pointe->bg; /* nouveau est suppose etre un */ - nouveau->ds = dernier->pointe->bd; /* module, donc bords=separateurs! */ - if (ancien->lastfils == dernier) - ancien->lastfils = premier; - /* ecrase l'ancier premier */ - nouveau->fpere = premier; - premier->pointe = nouveau; - premier->suiv = dernier->suiv; - /* met a jour dernier */ - dernier->suiv = NULL; - /* met a jour la liste des peres */ - f = nf; - while (f != dernier->suiv) { - f->pointe->pere = nouveau; /*avant c'etait ancien... */ - f->pointe->fpere = premier; - f = f->suiv; - } -} - -void printnoeud(noeud * N, int level) -{ - /* imprime recursivement l'arbre par parcours en profondeur */ - fils *ffils; - noeud *nfils; - int i; - ffils = N->fils; - - for (i = 0; i < level - 1; i++) - printf(" |"); - if (N->pere == NULL) - printf(" "); - else - printf(" +-"); - switch (N->type) { - case UNKN: - printf("Noeud\n"); - break; - case MODULE: - printf("Module\n"); - break; - case ARTEFACT: - printf("Artefact\n"); - break; - case SERIE: - printf("Serie \n"); - break; - case PARALLELE: - printf("Parallele \n"); - break; - case PREMIER: - printf("Premier \n"); - break; - } - - do { - nfils = ffils->pointe; - if (nfils->type == FEUILLE) { - for (i = 0; i < level; i++) - printf(" |"); - printf(" +--"); - printf("%i\n", 1 + nfils->nom); - } - else { - printnoeud(nfils, level + 1); - } - ffils = ffils->suiv; - } - while (ffils != NULL); -} - -void printarbre(noeud * N) -{ - printnoeud(N, 0); -} - -noeud *algo2(graphe G, sommet **S) -{ -/* algorithme de decomposition modulaire, deuxieme passe -entree: le graphe G, et sa permutation factorisante S. -sortie: un pointeur sur un arbre de decomposition modulaire -*/ - /* debug: S n'est utilise que pour mettre vrainom a jour */ - int n; //nombre de sommets du graphe - int *ouvrantes; /* tableau du nombre de parentheses ouvrantes */ - /* ouvrante[i]=3 ssi i-1(((i */ - /* ouvrante [0]=3: (((0 */ - - int *fermantes; /* idem fermantes[i]=2 ssi i)))i+1 - fermante [n-1]=2 ssi n))) */ - int *ps; /* ps[i]=premier separateur de (i,i+1) */ - int *ds; - - int i, j; /*indices de paires ou de sommets */ - - sadj *a1, *a2; /* parcours de liste d'adjacence */ - - noeud *racine; /*racine du pseudocoardre */ - noeud *courant, *nouveau; /* noeud courant du pseudocoarbre */ - noeud **pileinterne; /* pile des modules pour les passes 3,5,5 */ - int indicepileinterne = 0; /*pointeur dans cette pile */ - int taillepileinterne; /* taille de la pile apres la 2eme passe */ - - int *adjii; /* adjii[i]=1 ssi S[i] et S[i+1] sont */ - /* adjacents */ - /*PROPHASE : initialisations */ - n=G.n; - ouvrantes = (int *) fabmalloc(n * sizeof(int)); - fermantes = (int *) fabmalloc(n * sizeof(int)); - ps = (int *) fabmalloc(n * sizeof(int)); - ds = (int *) fabmalloc(n * sizeof(int)); - pileinterne = (noeud **) fabmalloc((2 * n + 4) * sizeof(noeud *)); - adjii= (int *) fabmalloc(n*sizeof(int)); - /*pas plus de 2n+4 noeuds internes dans le pseudocoarbre */ - for (i = 0; i < n; i++) { - ouvrantes[i] = 0; - fermantes[i] = 0; - adjii[i]=0; - } - - /* remplit adjii qui dit si S[i] adjacent a S[i+1] */ - for(i=0; iadj; - while((a1!=NULL)&&(a1->pointe->place != i+1)) - a1=a1->suiv; - if( a1 == NULL) - adjii[i]=0; - else // a1->pointe->place==i+1, donc i adj i+1 - adjii[i]=1; - } - adjii[n-1]=0; //perfectionnisme - - /* PREMIERE PASSE - on va parentheser la permutation factorisante. - tout bonnement, on lit S et on cherche les separateurs; - apres quoi ouvrantes et fermantes sont ecrites - complexite: O(n^2) */ - - ouvrantes[0] = 1; - fermantes[n - 1] = 1; /* parentheses des bords */ - - for (i = 0; i < n - 1; i++) { - /*recherche de ps(i,i+1) */ - a1=S[i]->adj; - a2=S[i+1]->adj; - while((a1!=NULL) && (a2!=NULL) && (a1->pointe->placepointe->placepointe->place == a2->pointe->place)) - { - a1=a1->suiv; - a2=a2->suiv; - } - - //arbre de decision complique pour trouver le premier separateur! - if( ((a1==NULL) && (a2==NULL)) - ||((a1==NULL) &&(a2->pointe->place >= i)) - ||((a2==NULL) && (a1->pointe->place >= i)) - ||((a1!=NULL) && (a2!=NULL) && (a1->pointe->place >= i) && (a2->pointe->place >= i))) - //pas de separateur - ps[i]=i+1; - else - { - if((a1==NULL) || (a1->pointe->place >= i)) - ps[i]=a2->pointe->place; - else if((a2==NULL) || (a2->pointe->place >= i)) - ps[i]=a1->pointe->place; - else - { - if((a1->suiv!=NULL)&&(a1->suiv->pointe->place == a2->pointe->place)) - ps[i]=a1->pointe->place; - else if ((a2->suiv!=NULL)&&(a2->suiv->pointe->place == a1->pointe->place)) - ps[i]=a2->pointe->place; - else - ps[i]=min(a1->pointe->place , a2->pointe->place); - } - ouvrantes[ps[i]]++; /* marque la fracture gauche, if any */ - fermantes[i]++; - } - if (DEBUG) - printf("ps(%i,%i)=%i\n", i , i+1, ps[i]); - - /*recherche de ds(i,i+1) - plus penible encore!*/ - a1=S[i]->adj; - if(a1!=NULL) // se place en queue de liste. - while(a1->suiv!=NULL) - a1=a1->suiv; - a2=S[i+1]->adj; - if(a2!=NULL) - while(a2->suiv!=NULL) - a2=a2->suiv; - while((a1!=NULL) && (a2!=NULL) && (a1->pointe->place > i+1) && - (a2->pointe->place > i+1) && (a1->pointe->place == a2->pointe->place)) - { - a1=a1->prec; - a2=a2->prec; - } - if( ((a1==NULL) && (a2==NULL)) - ||((a1==NULL) && (a2->pointe->place <= i+1)) - ||((a2==NULL) && (a1->pointe->place <= i+1)) - ||((a1!=NULL) && (a2!=NULL) && (a1->pointe->place <= i+1) && (a2->pointe->place <= i+1))) - //pas de separateur - ds[i]=i+1; - else - { - if((a1==NULL) || (a1->pointe->place <= i+1)) - ds[i]=a2->pointe->place; - else if((a2==NULL) || (a2->pointe->place <= i+1)) - ds[i]=a1->pointe->place; - else - { - if((a1->prec!=NULL)&&(a1->prec->pointe->place == a2->pointe->place)) - ds[i]=a1->pointe->place; - else if((a2->prec!=NULL)&&(a2->prec->pointe->place == a1->pointe->place)) - ds[i]=a2->pointe->place; - else - ds[i]=max(a1->pointe->place , a2->pointe->place); - } - - - //ds[i] = j; - ouvrantes[i + 1]++; /* marque la fracture gauche, if any */ - fermantes[ds[i]]++; /* attention aux decalages d'indices */ - } - if (DEBUG) - printf("ds(%i,%i)=%i\n", i,i+1,ds[i]); - //S[i]->nom + 1, S[i + 1]->nom + 1, S[ds[i]]->nom + 1); - } - - /*DEUXIEME PASSE: construction du pseudocoarbre */ - - racine = nouvnoeud(UNKN, NULL, -1, n); - courant = racine; - for (i = 0; i < n; i++) { - /*1: on lit des parentheses ouvrantes: descentes */ - for (j = 0; j < ouvrantes[i]; j++) { - /*Descente vers un nouveau noeud */ - nouveau = nouvnoeud(UNKN, courant, -1, n); - ajoutfils(courant, nouveau); /*on l'ajoute... */ - courant = nouveau; /* et on descent */ - if (DEBUG) - printf("("); - } - /* 2: on lit le sommet: feuille */ - nouveau = nouvnoeud(FEUILLE, courant, i, n); - ajoutfils(courant, nouveau); /*on ajoute le bebe... */ - (*nouveau).ps = i; - (*nouveau).ds = i; - (*nouveau).bg = i; - (*nouveau).bd = i; - nouveau->nom = S[i]->nom; - /* et pourquoi pas i ? Je m'embrouille... */ - if (DEBUG) - printf(" %i ", S[i]->nom + 1); - - /*3: on lit des parentheses fermantes: remontees */ - for (j = 0; j < fermantes[i]; j++) { - /*ASTUCE: ici on va en profiter pour supprimer les - noeuds a un fils, afin d'economiser une passe */ - if (courant->fils == courant->lastfils) { /*just one son */ - courant->pere->lastfils->pointe = courant->fils->pointe; - courant->fils->pointe->pere = courant->pere; - courant->fils->pointe->fpere = courant->pere->lastfils; - /* explication: le dernier fils de courant.pere est - actuellement courant himself. Il est donc pointe par - courant.pere.lastfils.pointe. Il suffit de changer ce - pointeur pour qu'il pointe maintenant non plus sur courant, - mais sur l'unique fils de courant: courant.fils.pointe. - Ouf! */ - /* NB: courant est maintenant deconnecte de l'arbre. - on pourrait faire un free() mais bon... */ - } - else { - /*on empile ce noeud interne. - L'ordre est celui de la postvisite d'un DFS */ - pileinterne[indicepileinterne] = courant; - indicepileinterne++; - } - /* et dans tous les cas: on remonte! */ - courant = courant->pere; - if (DEBUG) - printf(")"); - } - } - if (DEBUG) - printf("\n"); - - /* on enleve un ptit defaut */ - racine = racine->fils->pointe; - racine->pere = NULL; - racine->fpere = NULL; - if (DEBUG) { - printf("Arbre apres la deuxieme passe:\n"); - printnoeud(racine, 0); - } - - /*TROISIEME PASSE */ - /* A ce stade, on a un pseudocoarbre de racine racine, - sans noeuds a un fils, et dont les etiquettes sont - FEUIILLE ou UNKN. Il y a une pile des noeuds UNKN, stockes - dans l'ordre de la postvisite d'un parcours en profondeur. - On va s'en servir pour faire remonter les bords et les - separateurs de bas en haut */ - - taillepileinterne = indicepileinterne; - for (indicepileinterne = 0; indicepileinterne < taillepileinterne; - indicepileinterne++) { - noeud *scanne; - fils *nextfils; - noeud *succourant; - /* scanne est le noeud (pere) que l'on regarde; - nextfils parcours sa liste de fils; - courant est le fils actuellement examine et - succourant=succ(courant) */ - noeud *debutnoeud; - fils *debutfils; - /*deux variables utilise pour la recherche de jumeaux: - debut du bloc max */ - - scanne = pileinterne[indicepileinterne]; /*he oui, la pile */ - nextfils = scanne->fils; /*on commence au premier fils */ - do { - /*la boucle chiante... cf mon memoire de DEA */ - courant = nextfils->pointe; - /* bords */ - scanne->bg = min(scanne->bg, courant->bg); - scanne->bd = max(scanne->bd, courant->bd); - /*separateurs */ - scanne->ps = min(scanne->ps, courant->ps); - if (scanne->fils->pointe != courant) - /*ce n'est pas le premier fils */ - scanne->ps = min(scanne->ps, ps[(courant->bg) - 1]); - scanne->ds = max(scanne->ds, courant->ds); - if (scanne->lastfils->pointe != courant) - /*ce n'est pas le dernier fils */ - scanne->ds = max(scanne->ds, ds[courant->bd]); - - nextfils = nextfils->suiv; - } - while (nextfils != NULL); - - - if (DEBUG) - printf("noeud %i-%i: ps=%i ds=%i", 1 + scanne->bg, - 1 + scanne->bd, 1 + scanne->ps, 1 + scanne->ds); - - /* maintenant le test tout simple pour savoir si on a un module: */ - if (((scanne->ps) == (scanne->bg)) - && ((scanne->ds) == (scanne->bd))) { - /*on a un module */ - scanne->type = MODULE; - if (DEBUG) - printf(" Module.\n"); - } - else { - scanne->type = ARTEFACT; - if (DEBUG) - printf(" artefact.\n"); - } - } - - if (DEBUG) { - printf("Arbre apres la troisieme passe:\n"); - printnoeud(racine, 0); - } - - /* QUATRIEME PASSE */ - /* technique:on se contente de fusionner les artefacts a leurs parents - ca se fait de bas en haut grace a pileinterne (toujours elle!) */ - - for (indicepileinterne = 0; indicepileinterne < taillepileinterne; - indicepileinterne++) { - noeud *scanne; - scanne = pileinterne[indicepileinterne]; /*he oui, la pile */ - if (scanne->type == ARTEFACT) { - /*attention! La pile peut contenir des noeuds deconnectes */ - fusionne(scanne->pere, scanne); - if (DEBUG) - printf("Artefact elimine: %i-%i\n", 1 + scanne->bg, - 1 + scanne->bd); - } - } - if (DEBUG) { - printf("Arbre apres la quatrieme passe:\n"); - printnoeud(racine, 0); - } - - /* CINQIEME ET DERNIERE PASSE */ - /* on va typer les noeuds et extraire les fusions. - comment on fait? Ben, la pile.... */ - for (indicepileinterne = 0; indicepileinterne < taillepileinterne; - indicepileinterne++) { - noeud *scanne; - fils *nextfils; - noeud *succourant; - /* scanne est le noeud (pere) que l'on regarde; - nextfils parcours sa liste de fils; - courant est le fils actuellement examine et - succourant=succ(courant) */ - noeud *debutnoeud; - fils *debutfils; - /*deux variables utilise pour la recherche de jumeaux: - debut du bloc max */ - - scanne = pileinterne[indicepileinterne]; - if (scanne->type != MODULE) - continue; /* je traite que les modules */ - - nextfils = scanne->fils; /*on commence au premier fils */ - while (1) { - courant = nextfils->pointe; - succourant = nextfils->suiv->pointe; - if (ps[courant->bd] >= courant->bg - && ds[courant->bd] <= succourant->bd) { - /*Des jumeaux!! ->serie ou parallele! */ - /* on va determiner le bloc max de jumeaux consecutifs */ - debutnoeud = courant; - debutfils = nextfils; - while (ps[courant->bd] >= courant->bg && - ds[courant->bd] <= succourant->bd && - nextfils->suiv != NULL) { - nextfils = nextfils->suiv; - courant = nextfils->pointe; - if (nextfils->suiv == NULL) - break; - succourant = nextfils->suiv->pointe; - } - /*maintenant on connait la taille du bloc: il va de - debutnoeud a courant inclus, - et dans la liste des fils de scanne, - il s'etend de debutfils a nextfils inclus. - On extrait cette liste pour en faire les fils d'un - nouveau noeud... sauf si pas la peine! */ - if (debutfils == scanne->fils - && nextfils == scanne->lastfils) { - /* le noeud scanne etait serie ou parallele */ - if ( adjii[debutnoeud->bd] !=0) - scanne->type = SERIE; - else - scanne->type = PARALLELE; - } - else { - if ( adjii[debutnoeud->bd]!=0) - /*seule cette ligne distingue G de G' !! */ - { - nouveau = nouvnoeud(SERIE, scanne, -1, n); - if (DEBUG) - printf("Module serie extrait: %i-%i\n", - 1 + debutnoeud->bg, 1 + courant->bd); - } - else { - nouveau = nouvnoeud(PARALLELE, scanne, -1, n); - if (DEBUG) - printf("Module parallele extrait: %i-%i\n", - 1 + debutnoeud->bg, 1 + courant->bd); - } - extraire(scanne, nouveau, debutfils, nextfils); - } - } - if (scanne->type == MODULE) - scanne->type = PREMIER; - if (nextfils->suiv == NULL || nextfils->suiv->suiv == NULL - || nextfils->suiv->suiv->suiv == NULL) - break; - nextfils = nextfils->suiv; - } - } - if (DEBUG) { - printf("Arbre final:\n"); - printnoeud(racine, 0); - } - return racine; -} - -void PrintG(graphe G) -/* affiche le graphe */ -{ - int i,r; adj *a; - r=0; - for(i=0;is); - a=a->suiv; - } - printf("\n"); - } -} -void PrintGS(sommet **S, int n) -/* affiche le graphe trie selon S (celui utilise par algo2)*/ -{ - int i; sadj *a; - for(i=0;iadj; - while(a!=NULL) - { - printf("%i ", a->pointe->place); - a=a->suiv; - } - printf("\n"); - } -} - -void PrintS2(sommet **S, int n) - /* affiche la permutation factorisante */ -{ - int i; - printf( "Place (nouvelle num) "); - for(i=0;iplace); - printf("\nNom (ancienne num) : "); - for(i=0;inom); - printf("\n"); -} - - -/* la fonction principale; qui fait pas grand'chose....*/ -noeud *decomposition_modulaire(graphe G) -{ - - sommet **S; /* la permutation factorisante */ - noeud *Racine; /* le futur arbre de decomposition */ - - setbuf(stdout,NULL); - - S = algo1(G); /* premiere partie: calcul - de la permutation factorisante */ - - TrierTous(S,G.n,Calculm(G));/* Trie les listes d'adjacence selon S - */ - if(DEBUG) - { - PrintGS(S,G.n); - PrintS2(S,G.n); - } - Racine = algo2(G, S); /* deuxieme partie: calcul de l'arbre */ - return Racine; -} diff --git a/src/sage/graphs/modular_decomposition/src/dm_english.h b/src/sage/graphs/modular_decomposition/src/dm_english.h deleted file mode 100644 index 1987fa0d70c..00000000000 --- a/src/sage/graphs/modular_decomposition/src/dm_english.h +++ /dev/null @@ -1,117 +0,0 @@ -/****************************************************** - -Copyright 2004, 2010 Fabien de Montgolfier -fm@liafa.jussieu.fr - -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. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -**********************************************************/ - -/******************************************************** - -MODULAR DECOMPOSITION OF UNDIRECTED GRAPHS -by Fabien de Montgolfier - -The program dm.c offer a single function, -decomposition_modulaire(). - -The input is a graph, its output is its modular decomposition tree. - -Input graph is stored using adjacency lists, and trees using a pointers representation (see below) -********************************************************/ - -#include -#include - -/********************************** -Definition of graphs (for the input) -The graph has n vertices. Each vertex is numbered from 0 to n-1. -A graph is a structure (not an object-oriented program !). -If you declare "graphe Gr", Gr.n is the numer of vertices of Gr. -Gr.G is an array of size n. Gr.G[i] points the first element -of adjaceny list of vertex i (NULL if i is isolated) -An adjency list is an usual linked list (vertex;pointer to next). -Adjacency lists may be unsorted. - -WARNING : the input graph *MUST* be symmetric : if j belongs to adjacency list of i then i belongs to adjacency list of j. Graphs that do not respect this produce unpredictible and false results. -**********************************/ - - -/* structure for creating an adjacency list */ -typedef struct Adj { - int s; // number of the vertex - struct Adj *suiv; // adress of next pair in the list, NULL if last -} adj; - -typedef struct { - int n; //number of vertices of the graph - adj **G; // array of size n. G[i] points the first pair of the adjaceny list of vertex i - -} graphe; - -/******************************** -Output : definition of modular decomposition tree. -Each internal node is labelled SERIE (for series), PARALLELE (for parallel) or PREMIER (for prime) depending of the quotient's type. -Each leaf is labelled FEUILLE and also contains the vertex number of the leaf. -As the tree is an inclusion tree, the vertex-set corresponding to an internal node correspond to the vertices numbers of the leaves that descend from that tree. The function decomposition_modulaire() return a pointer to the root of the tree. - - - -/* define the type of nodes. UNKN,MODULE,ARTEFACT are for internal use*/ - -#define FEUILLE 0 // the node is a leaf -#define UNKN 1 -#define MODULE 2 -#define ARTEFACT 3 -#define SERIE 4 // series composition -#define PARALLELE 5 // parallel composition -#define PREMIER 6 // prime composition - -/* defines a node of the tree */ - -typedef struct Noeud { - int type; // is FEUILLE, SERIE, PARALLELE or PREMIER - struct Noeud *pere; // adress of parent node, NULL if root - struct Fils *fpere; // points the head of the linked list of sons (if type is not FEUILLE, else is NULL) - int ps; // internal use - int bg; // internal use - int ds; // internal use - int bd; // internal use - int sommet; // internal use - int nom; // if type=FEUILLE, number of the corresponding vertex of the graph - struct Fils *fils; // points the head of the linked list of sons - struct Fils *lastfils; // internal use (points the last item in the listed list of sons) - int id; // internal use (node unique ID) -} noeud; - -/* linked list that strore the sons of an internal node (in any order) */ - -typedef struct Fils { - struct Noeud *pointe; // adress of the node in the tree - struct Fils *suiv; // adress of the next pair in the list, NULL if last -} fils; - -/* prototype of the function. - Input is a graph, output the root of the modular decomposition tree */ - -noeud *decomposition_modulaire(graphe G); - - - - - - - - diff --git a/src/sage/graphs/modular_decomposition/src/random.c b/src/sage/graphs/modular_decomposition/src/random.c deleted file mode 100644 index 26951e28118..00000000000 --- a/src/sage/graphs/modular_decomposition/src/random.c +++ /dev/null @@ -1,149 +0,0 @@ -/****************************************************** - -Copyright 2004, 2010 Fabien de Montgolfier -fm@liafa.jussieu.fr - -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. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -**********************************************************/ - -#include -#include "dm_english.h" - -#define NIV 20 -#define VERBEUX 1 - -//extern noeud *decomposition_modulaire(int *,int); -extern void printarbre(noeud *); -/* ppm est la part par million d'arretes voulues dans le graphe */ - -void compte(noeud *N, int level, int *C) -{ - fils *F; - switch(N->type) - { - case SERIE: C[4*level]++; break; - case PARALLELE: C[4*level+1]++; break; - case PREMIER: C[4*level+2]++; break; - case FEUILLE: C[4*level+3]++; break; - } - if(N->type!=FEUILLE) - for(F=N->fils;F!=NULL;F=F->suiv) - compte(F->pointe, level+1, C); -} - -void test(int n, long int ppm, int *C) -{ - /*genere un graphe aleatoire, l'affiche, le decompose*/ - graphe G; - - int i,j; - adj *a; - noeud *R; - - - srandom((unsigned)time(NULL)); - - G.n=n; - G.G=(adj **)malloc(n*sizeof(adj *)); - - for(i=0;isuiv) - printf("%i ",1+a->s); - } - for(j=i+1;js=j; - a->suiv=G.G[i]; - G.G[i]=a; - // et reciproquement - a=(adj *)malloc(sizeof(adj)); - a->s=i; - a->suiv=G.G[j]; - G.G[j]=a; - if(VERBEUX) - printf("%i ",j+1); - } - } - if(VERBEUX) - printf("\n"); - } - - // appel de la fonction de decomposition - R = decomposition_modulaire(G); - - // affichage de l'arbre - if(VERBEUX) - printarbre(R); - - compte(R,0,C); - printf("Statistiques sur l'arbre de decomposition:\n"); - if(C[0]) - printf("La racine est Serie\n"); - else if(C[1]) - printf("La racine est Parrallele\n"); - else - printf("La racine est Premier \n"); - for(i=1 ; i': from sage.libs.flint.types cimport ulong -cdef extern from 'parisage.h': +cdef extern from "sage/libs/pari/parisage.h": char* PARIVERSION ctypedef long* GEN @@ -3931,7 +3931,7 @@ cdef extern from 'parisage.h': GEN uutoineg(ulong x, ulong y) long vali(GEN x) -cdef extern from 'parisage.h': +cdef extern from "sage/libs/pari/parisage.h": GEN set_gel(GEN x, long n, GEN z) # gel(x,n) = z GEN set_gmael(GEN x, long i, long j, GEN z) # gmael(x,i,j) = z GEN set_gcoeff(GEN x, long i, long j, GEN z) # gcoeff(x,i,j) = z diff --git a/src/sage/libs/pari/declinl.pxi b/src/sage/libs/pari/declinl.pxi index 5997218977c..1926a5e43e2 100644 --- a/src/sage/libs/pari/declinl.pxi +++ b/src/sage/libs/pari/declinl.pxi @@ -12,7 +12,7 @@ AUTHORS: """ -cdef extern from "parisage.h": +cdef extern from "sage/libs/pari/parisage.h": ################################################################### # # diff --git a/src/sage/libs/pari/gen.pxd b/src/sage/libs/pari/gen.pxd index 4941f395279..72f4d17d2d3 100644 --- a/src/sage/libs/pari/gen.pxd +++ b/src/sage/libs/pari/gen.pxd @@ -1,12 +1,16 @@ include 'decl.pxi' -cimport sage.structure.element +from sage.structure.element cimport RingElement cimport cython -@cython.final -cdef class gen(sage.structure.element.RingElement): + +cdef class gen_auto(RingElement): cdef GEN g cdef pari_sp b cdef dict refers_to +@cython.final +cdef class gen(gen_auto): + pass + cpdef gen objtogen(s) diff --git a/src/sage/libs/pari/gen.pyx b/src/sage/libs/pari/gen.pyx index 643de3d3fae..90c9962add5 100644 --- a/src/sage/libs/pari/gen.pyx +++ b/src/sage/libs/pari/gen.pyx @@ -64,18 +64,18 @@ cdef extern from "misc.h": from sage.libs.gmp.pylong cimport mpz_set_pylong -# Will be imported as needed -Integer = None +from pari_instance cimport PariInstance, prec_bits_to_words, pari_instance +cdef PariInstance P = pari_instance -import pari_instance -from pari_instance cimport PariInstance, prec_bits_to_words -cdef PariInstance P = pari_instance.pari +from sage.rings.integer cimport Integer +include 'auto_gen.pxi' + @cython.final -cdef class gen(sage.structure.element.RingElement): +cdef class gen(gen_auto): """ - Python extension class that models the PARI GEN type. + Cython extension class that models the PARI GEN type. """ def __init__(self): raise RuntimeError("PARI objects cannot be instantiated directly; use pari(x) to convert x to PARI") @@ -1306,12 +1306,7 @@ cdef class gen(sage.structure.element.RingElement): 9223372036854775807 # 64-bit sage: int(pari(RealField(63)(2^63+2))) 9223372036854775810L - """ - global Integer - if Integer is None: - import sage.rings.integer - Integer = sage.rings.integer.Integer return int(Integer(self)) def python_list_small(gen self): @@ -1429,10 +1424,6 @@ cdef class gen(sage.structure.element.RingElement): sage: long(pari("Mod(2, 7)")) 2L """ - global Integer - if Integer is None: - import sage.rings.integer - Integer = sage.rings.integer.Integer return long(Integer(self)) def __float__(gen self): @@ -1977,66 +1968,6 @@ cdef class gen(sage.structure.element.RingElement): ########################################### # 1: Standard monadic or dyadic OPERATORS ########################################### - def divrem(gen x, y, var=-1): - """ - divrem(x, y, v): Euclidean division of x by y giving as a - 2-dimensional column vector the quotient and the remainder, with - respect to v (to main variable if v is omitted). - """ - cdef gen t0 = objtogen(y) - pari_catch_sig_on() - return P.new_gen(divrem(x.g, t0.g, P.get_var(var))) - - def lex(gen x, y): - """ - lex(x,y): Compare x and y lexicographically (1 if xy, 0 if x==y, -1 - if xy) - """ - cdef gen t0 = objtogen(y) - pari_catch_sig_on() - r = lexcmp(x.g, t0.g) - pari_catch_sig_off() - return r - - def max(gen x, y): - """ - max(x,y): Return the maximum of x and y. - """ - cdef gen t0 = objtogen(y) - pari_catch_sig_on() - return P.new_gen(gmax(x.g, t0.g)) - - def min(gen x, y): - """ - min(x,y): Return the minimum of x and y. - """ - cdef gen t0 = objtogen(y) - pari_catch_sig_on() - return P.new_gen(gmin(x.g, t0.g)) - - def shift(gen x, long n): - """ - shift(x,n): shift x left n bits if n=0, right -n bits if n0. - """ - pari_catch_sig_on() - return P.new_gen(gshift(x.g, n)) - - def shiftmul(gen x, long n): - """ - shiftmul(x,n): Return the product of x by `2^n`. - """ - pari_catch_sig_on() - return P.new_gen(gmul2n(x.g, n)) - - def moebius(gen x): - """ - moebius(x): Moebius function of x. - """ - pari_catch_sig_on() - r = moebius(x.g) - pari_catch_sig_off() - return r - def sign(gen x): """ Return the sign of x, where x is of type integer, real or @@ -3466,12 +3397,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(gimag(x.g)) - def length(self): - """ - - """ - return glength(self.g) - def lift(gen x, v=-1): """ lift(x,v): Returns the lift of an element of Z/nZ to Z or R[x]/(P) @@ -3521,49 +3446,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(numbpart(x.g)) - def numerator(gen x): - """ - numerator(x): Returns the numerator of x. - - INPUT: - - - - ``x`` - gen - - - OUTPUT: gen - - EXAMPLES: - """ - pari_catch_sig_on() - return P.new_gen(numer(x.g)) - - - def numtoperm(gen k, long n): - """ - numtoperm(k, n): Return the permutation number k (mod n!) of n - letters, where n is an integer. - - INPUT: - - - - ``k`` - gen, integer - - - ``n`` - int - - - OUTPUT: - - - - ``gen`` - vector (permutation of 1,...,n) - - - EXAMPLES: - """ - pari_catch_sig_on() - return P.new_gen(numtoperm(n, k.g)) - - def padicprec(gen x, p): """ padicprec(x,p): Return the absolute p-adic precision of the object @@ -3627,31 +3509,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(gel(x.g, 2)) - def permtonum(gen x): - """ - permtonum(x): Return the ordinal (between 1 and n!) of permutation - vector x. ??? Huh ??? say more. what is a perm vector. 0 to n-1 or - 1-n. - - INPUT: - - - - ``x`` - gen (vector of integers) - - - OUTPUT: - - - - ``gen`` - integer - - - EXAMPLES: - """ - if typ(x.g) != t_VEC: - raise TypeError, "x (=%s) must be of type t_VEC, but is of type %s."%(x,x.type()) - pari_catch_sig_on() - return P.new_gen(permtonum(x.g)) - def precision(gen x, long n=-1): """ precision(x,n): Change the precision of x to be n, where n is a @@ -3674,47 +3531,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(precision0(x.g, n)) - def random(gen N): - r""" - ``random(N=2^31)``: Return a pseudo-random integer - between 0 and `N-1`. - - INPUT: - - - -``N`` - gen, integer - - - OUTPUT: - - - - ``gen`` - integer - - - EXAMPLES: - """ - if typ(N.g) != t_INT: - raise TypeError, "x (=%s) must be of type t_INT, but is of type %s."%(N,N.type()) - pari_catch_sig_on() - return P.new_gen(genrand(N.g)) - - def real(gen x): - """ - real(x): Return the real part of x. - - INPUT: - - - - ``x`` - gen - - - OUTPUT: gen - - EXAMPLES: - """ - pari_catch_sig_on() - return P.new_gen(greal(x.g)) - def round(gen x, estimate=False): """ round(x,estimate=False): If x is a real number, returns x rounded @@ -5379,26 +5195,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(binomial(x.g, k)) - def contfrac(gen x, b=0, long lmax=0): - """ - contfrac(x,b,lmax): continued fraction expansion of x (x rational, - real or rational function). b and lmax are both optional, where b - is the vector of numerators of the continued fraction, and lmax is - a bound for the number of terms in the continued fraction - expansion. - """ - cdef gen t0 = objtogen(b) - pari_catch_sig_on() - return P.new_gen(contfrac0(x.g, t0.g, lmax)) - - def contfracpnqn(gen x, b=0, long lmax=0): - """ - contfracpnqn(x): [p_n,p_n-1; q_n,q_n-1] corresponding to the - continued fraction x. - """ - pari_catch_sig_on() - return P.new_gen(pnqn(x.g)) - def ffgen(gen T, v=-1): r""" Return the generator `g=x \bmod T` of the finite field defined @@ -6984,49 +6780,10 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_off() return n - def bnfinit(self, long flag=0, tech=None, unsigned long precision=0): - cdef gen t0 - if tech is None: - pari_catch_sig_on() - return P.new_gen(bnfinit0(self.g, flag, NULL, prec_bits_to_words(precision))) - else: - t0 = objtogen(tech) - pari_catch_sig_on() - return P.new_gen(bnfinit0(self.g, flag, t0.g, prec_bits_to_words(precision))) - - def bnfisintnorm(self, x): - cdef gen t0 = objtogen(x) - pari_catch_sig_on() - return P.new_gen(bnfisintnorm(self.g, t0.g)) - - def bnfisnorm(self, x, long flag=0): - cdef gen t0 = objtogen(x) - pari_catch_sig_on() - return P.new_gen(bnfisnorm(self.g, t0.g, flag)) - - def bnfisprincipal(self, x, long flag=1): - cdef gen t0 = objtogen(x) - pari_catch_sig_on() - return P.new_gen(bnfisprincipal0(self.g, t0.g, flag)) - - def bnfnarrow(self): - pari_catch_sig_on() - return P.new_gen(buchnarrow(self.g)) - - def bnfsunit(self, S, unsigned long precision=0): - cdef gen t0 = objtogen(S) - pari_catch_sig_on() - return P.new_gen(bnfsunit(self.g, t0.g, prec_bits_to_words(precision))) - def bnfunit(self): pari_catch_sig_on() return P.new_gen(bnf_get_fu(self.g)) - def bnfisunit(self, x): - cdef gen t0 = objtogen(x) - pari_catch_sig_on() - return P.new_gen(bnfisunit(self.g, t0.g)) - def bnrclassno(self, I): r""" Return the order of the ray class group of self modulo ``I``. @@ -7049,23 +6806,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(bnrclassno(self.g, t0.g)) - def bnfissunit(self, sunit_data, x): - cdef gen t0 = objtogen(x) - cdef gen t1 = objtogen(sunit_data) - pari_catch_sig_on() - return P.new_gen(bnfissunit(self.g, t1.g, t0.g)) - - def dirzetak(self, n): - cdef gen t0 = objtogen(n) - pari_catch_sig_on() - return P.new_gen(dirzetak(self.g, t0.g)) - - def galoisapply(self, aut, x): - cdef gen t0 = objtogen(aut) - cdef gen t1 = objtogen(x) - pari_catch_sig_on() - return P.new_gen(galoisapply(self.g, t0.g, t1.g)) - def galoisinit(self, den=None): """ Calculate the Galois group of ``self``. @@ -7331,29 +7071,6 @@ cdef class gen(sage.structure.element.RingElement): P.clear_stack() return v - def idealred(self, I, vdir=0): - cdef gen t0 = objtogen(I) - cdef gen t1 = objtogen(vdir) - pari_catch_sig_on() - return P.new_gen(idealred0(self.g, t0.g, t1.g if vdir else NULL)) - - def idealadd(self, x, y): - cdef gen t0 = objtogen(x) - cdef gen t1 = objtogen(y) - pari_catch_sig_on() - return P.new_gen(idealadd(self.g, t0.g, t1.g)) - - def idealaddtoone(self, x, y): - cdef gen t0 = objtogen(x) - cdef gen t1 = objtogen(y) - pari_catch_sig_on() - return P.new_gen(idealaddtoone0(self.g, t0.g, t1.g)) - - def idealappr(self, x, long flag=0): - cdef gen t0 = objtogen(x) - pari_catch_sig_on() - return P.new_gen(idealappr0(self.g, t0.g, flag)) - def idealchinese(self, x, y): """ Chinese Remainder Theorem over number fields. @@ -7413,28 +7130,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(idealcoprime(self.g, t0.g, t1.g)) - def idealdiv(self, x, y, long flag=0): - cdef gen t0 = objtogen(x) - cdef gen t1 = objtogen(y) - pari_catch_sig_on() - return P.new_gen(idealdiv0(self.g, t0.g, t1.g, flag)) - - def idealfactor(self, x): - cdef gen t0 = objtogen(x) - pari_catch_sig_on() - return P.new_gen(idealfactor(self.g, t0.g)) - - def idealhnf(self, a, b=None): - cdef gen t0 = objtogen(a) - cdef gen t1 - if b is None: - pari_catch_sig_on() - return P.new_gen(idealhnf(self.g, t0.g)) - else: - t1 = objtogen(b) - pari_catch_sig_on() - return P.new_gen(idealhnf0(self.g, t0.g, t1.g)) - def idealintersection(self, x, y): cdef gen t0 = objtogen(x) cdef gen t1 = objtogen(y) @@ -7501,20 +7196,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(ideallog(self.g, t0.g, t1.g)) - def idealmul(self, x, y, long flag=0): - cdef gen t0 = objtogen(x) - cdef gen t1 = objtogen(y) - pari_catch_sig_on() - if flag == 0: - return P.new_gen(idealmul(self.g, t0.g, t1.g)) - else: - return P.new_gen(idealmulred(self.g, t0.g, t1.g)) - - def idealnorm(self, x): - cdef gen t0 = objtogen(x) - pari_catch_sig_on() - return P.new_gen(idealnorm(self.g, t0.g)) - def idealprimedec(nf, p): """ Prime ideal decomposition of the prime number `p` in the number @@ -7575,17 +7256,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(idealstar0(self.g, t0.g, flag)) - def idealtwoelt(self, x, a=None): - cdef gen t0 = objtogen(x) - cdef gen t1 - if a is None: - pari_catch_sig_on() - return P.new_gen(idealtwoelt0(self.g, t0.g, NULL)) - else: - t1 = objtogen(a) - pari_catch_sig_on() - return P.new_gen(idealtwoelt0(self.g, t0.g, t1.g)) - def idealval(self, x, p): cdef gen t0 = objtogen(x) cdef gen t1 = objtogen(p) @@ -7602,15 +7272,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_off() return v - def modreverse(self): - """ - modreverse(x): reverse polymod of the polymod x, if it exists. - - EXAMPLES: - """ - pari_catch_sig_on() - return P.new_gen(modreverse(self.g)) - def nfbasis(self, long flag=0, fa=None): """ Integral basis of the field `\QQ[a]`, where ``a`` is a root of @@ -7856,11 +7517,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(nfreduce(self.g, t0.g, t1.g)) - def nffactor(self, x): - cdef gen t0 = objtogen(x) - pari_catch_sig_on() - return P.new_gen(nffactor(self.g, t0.g)) - def nfgenerator(self): f = self[0] x = f.variable() @@ -7899,7 +7555,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_off() return r - def nfhnf(self,x): """ nfhnf(nf,x) : given a pseudo-matrix (A, I) or an integral pseudo-matrix (A,I,J), finds a @@ -7957,7 +7612,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(nfhnf(self.g, t0.g)) - def nfinit(self, long flag=0, unsigned long precision=0): """ nfinit(pol, {flag=0}): ``pol`` being a nonconstant irreducible @@ -8062,38 +7716,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(nfsubfields(self.g, d)) - def rnfcharpoly(self, T, a, v='x'): - cdef gen t0 = objtogen(T) - cdef gen t1 = objtogen(a) - cdef gen t2 = objtogen(v) - pari_catch_sig_on() - return P.new_gen(rnfcharpoly(self.g, t0.g, t1.g, gvar(t2.g))) - - def rnfdisc(self, x): - cdef gen t0 = objtogen(x) - pari_catch_sig_on() - return P.new_gen(rnfdiscf(self.g, t0.g)) - - def rnfeltabstorel(self, x): - cdef gen t0 = objtogen(x) - pari_catch_sig_on() - return P.new_gen(rnfeltabstorel(self.g, t0.g)) - - def rnfeltreltoabs(self, x): - cdef gen t0 = objtogen(x) - pari_catch_sig_on() - return P.new_gen(rnfeltreltoabs(self.g, t0.g)) - - def rnfequation(self, poly, long flag=0): - cdef gen t0 = objtogen(poly) - pari_catch_sig_on() - return P.new_gen(rnfequation0(self.g, t0.g, flag)) - - def rnfidealabstorel(self, x): - cdef gen t0 = objtogen(x) - pari_catch_sig_on() - return P.new_gen(rnfidealabstorel(self.g, t0.g)) - def rnfidealdown(self, x): r""" rnfidealdown(rnf,x): finds the intersection of the ideal x with the base field. @@ -8119,26 +7741,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(rnfidealdown(self.g, t0.g)) - def rnfidealhnf(self, x): - cdef gen t0 = objtogen(x) - pari_catch_sig_on() - return P.new_gen(rnfidealhnf(self.g, t0.g)) - - def rnfidealnormrel(self, x): - cdef gen t0 = objtogen(x) - pari_catch_sig_on() - return P.new_gen(rnfidealnormrel(self.g, t0.g)) - - def rnfidealreltoabs(self, x): - cdef gen t0 = objtogen(x) - pari_catch_sig_on() - return P.new_gen(rnfidealreltoabs(self.g, t0.g)) - - def rnfidealtwoelt(self, x): - cdef gen t0 = objtogen(x) - pari_catch_sig_on() - return P.new_gen(rnfidealtwoelement(self.g, t0.g)) - def rnfinit(self, poly): """ EXAMPLES: We construct a relative number field. @@ -8155,13 +7757,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(rnfinit(self.g, t0.g)) - def rnfisfree(self, poly): - cdef gen t0 = objtogen(poly) - pari_catch_sig_on() - r = rnfisfree(self.g, t0.g) - pari_catch_sig_off() - return r - def quadhilbert(self): r""" Returns a polynomial over `\QQ` whose roots generate the @@ -8210,10 +7805,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(content(self.g)) - def deriv(self, v=-1): - pari_catch_sig_on() - return P.new_gen(deriv(self.g, P.get_var(v))) - def eval(self, *args, **kwds): """ Evaluate ``self`` with the given arguments. @@ -8494,34 +8085,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(factorpadic(self.g, t0.g, r)) - def factormod(self, p, long flag=0): - """ - x.factormod(p,flag=0): factorization mod p of the polynomial x - using Berlekamp. flag is optional, and can be 0: default or 1: - simple factormod, same except that only the degrees of the - irreducible factors are given. - """ - cdef gen t0 = objtogen(p) - pari_catch_sig_on() - return P.new_gen(factormod0(self.g, t0.g, flag)) - - def intformal(self, y=-1): - """ - x.intformal(y): formal integration of x with respect to the main - variable of y, or to the main variable of x if y is omitted - """ - pari_catch_sig_on() - return P.new_gen(integ(self.g, P.get_var(y))) - - def padicappr(self, a): - """ - x.padicappr(a): p-adic roots of the polynomial x congruent to a mod - p - """ - cdef gen t0 = objtogen(a) - pari_catch_sig_on() - return P.new_gen(padicappr(self.g, t0.g)) - def newtonpoly(self, p): """ x.newtonpoly(p): Newton polygon of polynomial x with respect to the @@ -8556,11 +8119,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(polcoeff0(self.g, n, P.get_var(var))) - def polcompositum(self, pol2, long flag=0): - cdef gen t0 = objtogen(pol2) - pari_catch_sig_on() - return P.new_gen(polcompositum0(self.g, t0.g, flag)) - def poldegree(self, var=-1): """ f.poldegree(var=x): Return the degree of this polynomial. @@ -8592,17 +8150,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(poldisc0(self.g, P.get_var(var))) - def poldiscreduced(self): - pari_catch_sig_on() - return P.new_gen(reduceddiscsmith(self.g)) - - def polgalois(self, unsigned long precision=0): - """ - f.polgalois(): Galois group of the polynomial f - """ - pari_catch_sig_on() - return P.new_gen(polgalois(self.g, prec_bits_to_words(precision))) - def nfgaloisconj(self, long flag=0, denom=None, unsigned long precision=0): r""" Edited from the pari documentation: @@ -8655,17 +8202,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(nfroots(self.g, t0.g)) - def polhensellift(self, y, p, long e): - """ - self.polhensellift(y, p, e): lift the factorization y of self - modulo p to a factorization modulo `p^e` using Hensel lift. - The factors in y must be pairwise relatively prime modulo p. - """ - cdef gen t0 = objtogen(y) - cdef gen t1 = objtogen(p) - pari_catch_sig_on() - return P.new_gen(polhensellift(self.g, t0.g, t1.g, e)) - def polisirreducible(self): """ f.polisirreducible(): Returns True if f is an irreducible @@ -8676,43 +8212,6 @@ cdef class gen(sage.structure.element.RingElement): P.clear_stack() return t != 0 - def pollead(self, v=-1): - """ - self.pollead(v): leading coefficient of polynomial or series self, - or self itself if self is a scalar. Error otherwise. With respect - to the main variable of self if v is omitted, with respect to the - variable v otherwise - """ - pari_catch_sig_on() - return P.new_gen(pollead(self.g, P.get_var(v))) - - def polrecip(self): - pari_catch_sig_on() - return P.new_gen(polrecip(self.g)) - - def polred(self, long flag=0, fa=None): - cdef gen t0 - if fa is None: - pari_catch_sig_on() - return P.new_gen(polred0(self.g, flag, NULL)) - else: - t0 = objtogen(fa) - pari_catch_sig_on() - return P.new_gen(polred0(self.g, flag, t0.g)) - - def polredabs(self, long flag=0): - pari_catch_sig_on() - return P.new_gen(polredabs0(self.g, flag)) - - def polredbest(self, long flag=0): - pari_catch_sig_on() - return P.new_gen(polredbest(self.g, flag)) - - def polresultant(self, y, var=-1, long flag=0): - cdef gen t0 = objtogen(y) - pari_catch_sig_on() - return P.new_gen(polresultant0(self.g, t0.g, P.get_var(var), flag)) - def polroots(self, long flag=-1, unsigned long precision=0): """ Complex roots of the given polynomial using Schonhage's method, @@ -8724,16 +8223,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(cleanroots(self.g, prec_bits_to_words(precision))) - def polrootsmod(self, p, long flag=0): - cdef gen t0 = objtogen(p) - pari_catch_sig_on() - return P.new_gen(rootmod0(self.g, t0.g, flag)) - - def polrootspadic(self, p, r=20): - cdef gen t0 = objtogen(p) - pari_catch_sig_on() - return P.new_gen(rootpadic(self.g, t0.g, r)) - def polrootspadicfast(self, p, r=20): from sage.misc.superseded import deprecation deprecation(16997, 'polrootspadicfast is deprecated, use polrootspadic or the direct PARI call ZpX_roots instead') @@ -8741,38 +8230,12 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(rootpadic(self.g, t0.g, r)) - def polsturm(self, a, b): - cdef gen t0 = objtogen(a) - cdef gen t1 = objtogen(b) - pari_catch_sig_on() - n = sturmpart(self.g, t0.g, t1.g) - pari_catch_sig_off() - return n - def polsturm_full(self): pari_catch_sig_on() n = sturmpart(self.g, NULL, NULL) pari_catch_sig_off() return n - def polsylvestermatrix(self, g): - cdef gen t0 = objtogen(g) - pari_catch_sig_on() - return P.new_gen(sylvestermatrix(self.g, t0.g)) - - def polsym(self, long n): - pari_catch_sig_on() - return P.new_gen(polsym(self.g, n)) - - def serconvol(self, g): - cdef gen t0 = objtogen(g) - pari_catch_sig_on() - return P.new_gen(convol(self.g, t0.g)) - - def serlaplace(self): - pari_catch_sig_on() - return P.new_gen(laplace(self.g)) - def serreverse(self): """ serreverse(f): reversion of the power series f. @@ -8794,16 +8257,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(serreverse(self.g)) - def thueinit(self, long flag=0, unsigned long precision=0): - pari_catch_sig_on() - return P.new_gen(thueinit(self.g, flag, prec_bits_to_words(precision))) - - - def rnfisnorminit(self, polrel, long flag=2): - cdef gen t0 = objtogen(polrel) - pari_catch_sig_on() - return P.new_gen(rnfisnorminit(self.g, t0.g, flag)) - def rnfisnorm(self, T, long flag=0): cdef gen t0 = objtogen(T) pari_catch_sig_on() @@ -8898,34 +8351,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(adj(self.g)).Mat() - def qflll(self, long flag=0): - """ - qflll(x,flag=0): LLL reduction of the vectors forming the matrix x - (gives the unimodular transformation matrix). The columns of x must - be linearly independent, unless specified otherwise below. flag is - optional, and can be 0: default, 1: assumes x is integral, columns - may be dependent, 2: assumes x is integral, returns a partially - reduced basis, 4: assumes x is integral, returns [K,I] where K is - the integer kernel of x and I the LLL reduced image, 5: same as 4 - but x may have polynomial coefficients, 8: same as 0 but x may have - polynomial coefficients. - """ - pari_catch_sig_on() - return P.new_gen(qflll0(self.g,flag)).Mat() - - def qflllgram(self, long flag=0): - """ - qflllgram(x,flag=0): LLL reduction of the lattice whose gram matrix - is x (gives the unimodular transformation matrix). flag is optional - and can be 0: default,1: lllgramint algorithm for integer matrices, - 4: lllgramkerim giving the kernel and the LLL reduced image, 5: - lllgramkerimgen same when the matrix has polynomial coefficients, - 8: lllgramgen, same as qflllgram when the coefficients are - polynomials. - """ - pari_catch_sig_on() - return P.new_gen(qflllgram0(self.g,flag)).Mat() - def lllgram(self): return self.qflllgram(0) @@ -9523,19 +8948,6 @@ cdef class gen(sage.structure.element.RingElement): # misc (classify when I know where they go) ########################################### - def hilbert(x, y, p): - cdef gen t0 = objtogen(y) - cdef gen t1 = objtogen(p) - pari_catch_sig_on() - r = hilbert(x.g, t0.g, t1.g) - P.clear_stack() - return r - - def chinese(self, y): - cdef gen t0 = objtogen(y) - pari_catch_sig_on() - return P.new_gen(chinese(self.g, t0.g)) - def order(self): pari_catch_sig_on() return P.new_gen(order(self.g)) @@ -9604,10 +9016,6 @@ cdef class gen(sage.structure.element.RingElement): def __abs__(self): return self.abs() - def norm(gen self): - pari_catch_sig_on() - return P.new_gen(gnorm(self.g)) - def nextprime(gen self, bint add_one=0): """ nextprime(x): smallest pseudoprime greater than or equal to `x`. @@ -9703,12 +9111,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(gsubst(self.g, P.get_var(var), t0.g)) - def substpol(self, y, z): - cdef gen t0 = objtogen(y) - cdef gen t1 = objtogen(z) - pari_catch_sig_on() - return P.new_gen(gsubstpol(self.g, t0.g, t1.g)) - def nf_subst(self, z): """ Given a PARI number field ``self``, return the same PARI @@ -9750,33 +9152,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(gsubst(self.g, gvar(self.g), t0.g)) - def taylor(self, v=-1): - pari_catch_sig_on() - return P.new_gen(tayl(self.g, P.get_var(v), precdl)) - - def thue(self, rhs, ne): - cdef gen t0 = objtogen(rhs) - cdef gen t1 = objtogen(ne) - pari_catch_sig_on() - return P.new_gen(thue(self.g, t0.g, t1.g)) - - def charpoly(self, var=-1, long flag=0): - """ - charpoly(A,v=x,flag=0): det(v\*Id-A) = characteristic polynomial of - A using the comatrix. flag is optional and may be set to 1 (use - Lagrange interpolation) or 2 (use Hessenberg form), 0 being the - default. - """ - pari_catch_sig_on() - return P.new_gen(charpoly0(self.g, P.get_var(var), flag)) - - def kronecker(gen self, y): - cdef gen t0 = objtogen(y) - pari_catch_sig_on() - r = kronecker(self.g, t0.g) - P.clear_stack() - return r - def type(gen self): """ Return the PARI type of self as a string. @@ -9834,7 +9209,6 @@ cdef class gen(sage.structure.element.RingElement): else: raise TypeError, "Unknown PARI type: %s"%t - def polinterpolate(self, ya, x): """ self.polinterpolate(ya,x,e): polynomial interpolation at x @@ -9868,15 +9242,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(algdep(self.g, n)) - def concat(self, y): - cdef gen t0 = objtogen(y) - pari_catch_sig_on() - return P.new_gen(concat(self.g, t0.g)) - - def lindep(self, long flag=0): - pari_catch_sig_on() - return P.new_gen(lindep0(self.g, flag)) - def listinsert(self, obj, long n): cdef gen t0 = objtogen(obj) pari_catch_sig_on() @@ -9887,8 +9252,6 @@ cdef class gen(sage.structure.element.RingElement): pari_catch_sig_on() return P.new_gen(listput(self.g, t0.g, n)) - - def elleisnum(self, long k, long flag=0, unsigned long precision=0): """ om.elleisnum(k, flag=0): om=[om1,om2] being a 2-component vector diff --git a/src/sage/libs/pari/pari_instance.pxd b/src/sage/libs/pari/pari_instance.pxd index b183ffd7113..e5909ccc4dc 100644 --- a/src/sage/libs/pari/pari_instance.pxd +++ b/src/sage/libs/pari/pari_instance.pxd @@ -2,15 +2,18 @@ include 'decl.pxi' from sage.libs.gmp.types cimport * from sage.libs.flint.types cimport fmpz_mat_t -cimport sage.structure.parent_base +from sage.structure.parent_base cimport ParentWithBase cimport cython from sage.libs.pari.gen cimport gen cpdef long prec_bits_to_words(unsigned long prec_in_bits) +cdef class PariInstance_auto(ParentWithBase): + pass + @cython.final -cdef class PariInstance(sage.structure.parent_base.ParentWithBase): +cdef class PariInstance(PariInstance_auto): cdef long _real_precision cdef gen PARI_ZERO, PARI_ONE, PARI_TWO cdef inline gen new_gen(self, GEN x) diff --git a/src/sage/libs/pari/pari_instance.pyx b/src/sage/libs/pari/pari_instance.pyx index 9451dba0ef9..5f91c0123d1 100644 --- a/src/sage/libs/pari/pari_instance.pyx +++ b/src/sage/libs/pari/pari_instance.pyx @@ -370,8 +370,10 @@ cdef void sage_flush(): sys.stdout.flush() +include 'auto_instance.pxi' + @cython.final -cdef class PariInstance(sage.structure.parent_base.ParentWithBase): +cdef class PariInstance(PariInstance_auto): def __init__(self, long size=1000000, unsigned long maxprime=500000): """ Initialize the PARI system. @@ -977,16 +979,16 @@ cdef class PariInstance(sage.structure.parent_base.ParentWithBase): return x - cdef long get_var(self, v): """ - Converts a Python string into a PARI variable reference number. Or - if v = -1, returns -1. + Convert a Python string into a PARI variable number. """ - if v != -1: - s = str(v) - return fetch_user_var(s) - return -1 + if v is None: + return -1 + if v == -1: + return -1 + cdef bytes s = bytes(v) + return fetch_user_var(s) ############################################################ # Initialization @@ -1260,15 +1262,6 @@ cdef class PariInstance(sage.structure.parent_base.ParentWithBase): self.init_primes(n+1) return self.prime_list(pari(n).primepi()) -## cdef long k -## k = (n+10)/math.log(n) -## p = 2 -## while p <= n: -## p = self.nth_prime(k) -## k = 2 -## v = self.prime_list(k) -## return v[:pari(n).primepi()] - def __nth_prime(self, long n): """ nth_prime(n): returns the n-th prime, where n is a C-int @@ -1288,7 +1281,6 @@ cdef class PariInstance(sage.structure.parent_base.ParentWithBase): self.init_primes(max(2*maxprime(), 20*n)) return self.nth_prime(n) - def euler(self, unsigned long precision=0): """ Return Euler's constant to the requested real precision (in bits). @@ -1429,10 +1421,6 @@ cdef class PariInstance(sage.structure.parent_base.ParentWithBase): return plist #return self.new_gen(polsubcyclo(n, d, self.get_var(v))) - def polzagier(self, long n, long m): - pari_catch_sig_on() - return self.new_gen(polzag(n, m)) - def setrand(self, seed): """ Sets PARI's current random number seed. diff --git a/src/c_lib/include/parisage.h b/src/sage/libs/pari/parisage.h similarity index 100% rename from src/c_lib/include/parisage.h rename to src/sage/libs/pari/parisage.h diff --git a/src/sage/libs/singular/singular-cdefs.pxi b/src/sage/libs/singular/singular-cdefs.pxi index 5ab2708aba4..4914ffb6a9b 100644 --- a/src/sage/libs/singular/singular-cdefs.pxi +++ b/src/sage/libs/singular/singular-cdefs.pxi @@ -6,11 +6,14 @@ AUTHOR: Martin Albrecht NOTE: our ring, poly etc. types are not the SINGULAR ring, poly, etc. types. They are deferences. So a SINGULAR ring is a ring pointer here. - """ + include "sage/ext/stdsage.pxi" include "sage/ext/cdefs.pxi" +cdef extern from "ccobject.h": + pass + cdef extern from "stdlib.h": void delete "delete" (void *ptr) diff --git a/src/sage/matrix/benchmark.py b/src/sage/matrix/benchmark.py index 636c5904118..93eb2d2f1d5 100644 --- a/src/sage/matrix/benchmark.py +++ b/src/sage/matrix/benchmark.py @@ -19,7 +19,7 @@ from constructor import random_matrix, Matrix from sage.rings.all import ZZ, QQ, GF from sage.misc.misc import alarm, cancel_alarm, cputime -from sage.ext.c_lib import AlarmInterrupt +from sage.ext.interrupt import AlarmInterrupt from sage.interfaces.all import magma diff --git a/src/sage/matrix/matrix.pyx b/src/sage/matrix/matrix.pyx index 81fa6ca45d3..7c198a9bf0c 100644 --- a/src/sage/matrix/matrix.pyx +++ b/src/sage/matrix/matrix.pyx @@ -13,7 +13,7 @@ For design documentation see matrix/docs.py. # http://www.gnu.org/licenses/ ################################################################################ -include 'sage/ext/stdsage.pxi' +from sage.ext.stdsage cimport PY_SET_TP_NEW def is_Matrix(x): """ diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 91e4502f627..a1d0a840cfa 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -6780,8 +6780,13 @@ cdef class Matrix(matrix1.Matrix): return extended def weak_popov_form(self, ascend=True): + from sage.misc.superseded import deprecation + deprecation(16888, 'You can just call row_reduced_form() instead') + return self.row_reduced_form(ascend) + + def row_reduced_form(self, ascend=True): """ - This function computes a weak Popov form of a matrix over a rational + This function computes a row reduced form of a matrix over a rational function field `k(x)`, for `k` a field. INPUT: @@ -6794,7 +6799,7 @@ cdef class Matrix(matrix1.Matrix): A 3-tuple `(W,N,d)` consisting of: - 1. `W` - a matrix over `k(x)` giving a weak the Popov form of self + 1. `W` - a matrix over `k(x)` giving a row reduced form of `self` 2. `N` - a matrix over `k[x]` representing row operations used to transform `self` to `W` 3. `d` - degree of respective columns of W; the degree of a column is @@ -6814,7 +6819,7 @@ cdef class Matrix(matrix1.Matrix): sage: R. = GF(3)['t'] sage: K = FractionField(R) sage: M = matrix([[(t-1)^2/t],[(t-1)]]) - sage: M.weak_popov_form() + sage: M.row_reduced_form() ( [ 0] [ t 2*t + 1] [(2*t + 1)/t], [ 1 2], [-Infinity, 0] @@ -6827,7 +6832,7 @@ cdef class Matrix(matrix1.Matrix): :: sage: M1 = matrix([[t*(t-1)*(t+1)],[t*(t-2)*(t+2)],[t]]) - sage: output1 = M1.weak_popov_form() + sage: output1 = M1.row_reduced_form() sage: output1 ( [0] [ 1 0 2*t^2 + 1] @@ -6842,7 +6847,7 @@ cdef class Matrix(matrix1.Matrix): :: sage: M2 = M1.change_ring(K) - sage: output2 = M2.weak_popov_form() + sage: output2 = M2.row_reduced_form() sage: output1 == output2 True sage: output1[0].base_ring() is K @@ -6861,7 +6866,7 @@ cdef class Matrix(matrix1.Matrix): sage: R. = QQ['t'] sage: M = matrix([[t^3 - t,t^2 - 2],[0,t]]).transpose() - sage: M.weak_popov_form() + sage: M.row_reduced_form() ( [ t -t^2] [ 1 -t] [t^2 - 2 t], [ 0 1], [2, 2] @@ -6874,7 +6879,7 @@ cdef class Matrix(matrix1.Matrix): sage: R. = GF(5)['t'] sage: K = FractionField(R) sage: M = matrix([[K(0),K(0)],[K(0),K(0)]]) - sage: M.weak_popov_form() + sage: M.row_reduced_form() ( [0 0] [1 0] [0 0], [0 1], [-Infinity, -Infinity] @@ -6886,7 +6891,7 @@ cdef class Matrix(matrix1.Matrix): sage: R. = QQ['t'] sage: M = matrix([[t,t,t],[0,0,t]], ascend=False) - sage: M.weak_popov_form() + sage: M.row_reduced_form() ( [t t t] [1 0] [0 0 t], [0 1], [1, 1] @@ -6898,7 +6903,7 @@ cdef class Matrix(matrix1.Matrix): :: sage: M = matrix([[1,0],[1,1]]) - sage: M.weak_popov_form() + sage: M.row_reduced_form() Traceback (most recent call last): ... TypeError: the coefficients of M must lie in a univariate @@ -6911,26 +6916,18 @@ cdef class Matrix(matrix1.Matrix): for row operations; however, references such as [H] transpose and use column operations. - - There are multiple weak Popov forms of a matrix, so one may want to - extend this code to produce a Popov form (see section 1.2 of [V]). The - latter is canonical, but more work to produce. - REFERENCES: .. [H] F. Hess, "Computing Riemann-Roch spaces in algebraic function fields and related topics," J. Symbolic Comput. 33 (2002), no. 4, 425--445. - .. [MS] T. Mulders, A. Storjohann, "On lattice reduction for polynomial - matrices," J. Symbolic Comput. 35 (2003), no. 4, 377--401 + .. [K] T. Kaliath, "Linear Systems", Prentice-Hall, 1980, 383--386. - .. [V] G. Villard, "Computing Popov and Hermite forms of polynomial - matrices", ISSAC '96: Proceedings of the 1996 international symposium - on Symbolic and algebraic computation, 250--258. """ import sage.matrix.matrix_misc - return sage.matrix.matrix_misc.weak_popov_form(self) + return sage.matrix.matrix_misc.row_reduced_form(self) ########################################################################## # Functions for symmetries of a matrix under row and column permutations # diff --git a/src/sage/matrix/matrix_misc.py b/src/sage/matrix/matrix_misc.py index 966a7228ea5..d07725467ea 100644 --- a/src/sage/matrix/matrix_misc.py +++ b/src/sage/matrix/matrix_misc.py @@ -28,6 +28,11 @@ def row_iterator(A): yield A.row(i) def weak_popov_form(M,ascend=True): + from sage.misc.superseded import deprecation + deprecation(16888, 'You can just call row_reduced_form() instead') + return row_reduced_form(M,ascend) + +def row_reduced_form(M,ascend=True): """ This function computes a weak Popov form of a matrix over a rational function field `k(x)`, for `k` a field. @@ -65,7 +70,7 @@ def weak_popov_form(M,ascend=True): sage: R. = GF(3)['t'] sage: K = FractionField(R) sage: import sage.matrix.matrix_misc - sage: sage.matrix.matrix_misc.weak_popov_form(matrix([[(t-1)^2/t],[(t-1)]])) + sage: sage.matrix.matrix_misc.row_reduced_form(matrix([[(t-1)^2/t],[(t-1)]])) ( [ 0] [ t 2*t + 1] [(2*t + 1)/t], [ 1 2], [-Infinity, 0] @@ -73,7 +78,7 @@ def weak_popov_form(M,ascend=True): NOTES: - See docstring for weak_popov_form method of matrices for + See docstring for row_reduced_form method of matrices for more information. """ diff --git a/src/sage/matrix/matrix_space.py b/src/sage/matrix/matrix_space.py index 973ef78e33d..0e8930f2bb7 100644 --- a/src/sage/matrix/matrix_space.py +++ b/src/sage/matrix/matrix_space.py @@ -1307,6 +1307,16 @@ def matrix(self, x=0, coerce=True, copy=True): sage: MatrixSpace(Qp(3),1,1)([Qp(3)(4/3)]) [3^-1 + 1 + O(3^19)] + One-rowed matrices over combinatorial free modules used to break + the constructor (:trac:`17124`). Check that this is fixed:: + + sage: Sym = SymmetricFunctions(QQ) + sage: h = Sym.h() + sage: MatrixSpace(h,1,1)([h[1]]) + [h[1]] + sage: MatrixSpace(h,2,1)([h[1], h[2]]) + [h[1]] + [h[2]] """ if x is None or isinstance(x, (int, integer.Integer)) and x == 0: if self._copy_zero: # faster to copy than to create a new one. @@ -1348,7 +1358,15 @@ def matrix(self, x=0, coerce=True, copy=True): for v in x: l = len(new_x) try: - new_x.extend(v) + from sage.structure.element import is_Vector + if isinstance(v, (list, tuple)) or is_Vector(v): + # The isinstance check should prevent the "flattening" + # of v if v is an iterable but not meant to be + # iterated (e.g., an element of a combinatorial free + # module). + new_x.extend(v) + else: + raise TypeError if len(new_x) - l != n: raise TypeError except TypeError: diff --git a/src/sage/misc/cython.py b/src/sage/misc/cython.py index b996a64dcf1..d50124f9459 100644 --- a/src/sage/misc/cython.py +++ b/src/sage/misc/cython.py @@ -196,7 +196,7 @@ def pyx_preparse(s): sage: from sage.misc.cython import pyx_preparse sage: pyx_preparse("") - ('\ninclude "interrupt.pxi" # ctrl-c interrupt block support\ninclude "stdsage.pxi" # ctrl-c interrupt block support\n\ninclude "cdefs.pxi"\n', + ('\ninclude "interrupt.pxi" # ctrl-c interrupt block support\ninclude "stdsage.pxi"\n\ninclude "cdefs.pxi"\n', ['mpfr', 'gmp', 'gmpxx', @@ -270,8 +270,7 @@ def pyx_preparse(s): v, s = parse_keywords('cinclude', s) inc = [environ_parse(x.replace('"','').replace("'","")) for x in v] + include_dirs s = """\ninclude "cdefs.pxi"\n""" + s - if lang != "c++": # has issues with init_csage() - s = """\ninclude "interrupt.pxi" # ctrl-c interrupt block support\ninclude "stdsage.pxi" # ctrl-c interrupt block support\n""" + s + s = """\ninclude "interrupt.pxi" # ctrl-c interrupt block support\ninclude "stdsage.pxi"\n""" + s args, s = parse_keywords('cargs', s) args = ['-w','-O2'] + args diff --git a/src/sage/misc/sageinspect.py b/src/sage/misc/sageinspect.py index aef66173c07..d33057b06cb 100644 --- a/src/sage/misc/sageinspect.py +++ b/src/sage/misc/sageinspect.py @@ -193,7 +193,7 @@ def _extract_embedded_position(docstring): sage: print open(_extract_embedded_position(inspect.getdoc(test_funct))[1]).read() include "interrupt.pxi" # ctrl-c interrupt block support - include "stdsage.pxi" # ctrl-c interrupt block support + include "stdsage.pxi" include "cdefs.pxi" cpdef test_funct(x,y): return diff --git a/src/sage/modular/dirichlet.py b/src/sage/modular/dirichlet.py index 8ddf3b417f0..9f254a1989f 100644 --- a/src/sage/modular/dirichlet.py +++ b/src/sage/modular/dirichlet.py @@ -274,8 +274,7 @@ def __call__(self, m): .. warning:: A table of values of the character is made the first time - you call this. This table is currently constructed in a - somewhat stupid way, though it is still pretty fast. + you call this (unless `m` equals -1) EXAMPLES:: @@ -283,6 +282,8 @@ def __call__(self, m): sage: e = prod(G.gens(), G(1)) sage: e Dirichlet character modulo 60 of conductor 60 mapping 31 |--> -1, 41 |--> -1, 37 |--> zeta4 + sage: e(-1) + -1 sage: e(2) 0 sage: e(7) @@ -300,18 +301,11 @@ def __call__(self, m): sage: parent(e(31*37)) Cyclotomic Field of order 4 and degree 2 """ - m = int(m%self.__modulus) - try: - return self.__values[m] - except AttributeError: - pass - - val = self.__modulus - 1 - if m == val: - return self.__eval_at_minus_one() + m = int(m % self.__modulus) + if self.values.is_in_cache() or m != self.__modulus - 1: + return self.values()[m] else: - self.values() # compute all values - return self.__values[m] + return self.__eval_at_minus_one() def change_ring(self, R): """ @@ -1495,9 +1489,10 @@ def restrict(self, M): H = DirichletGroup(M, self.base_ring()) return H(self) + @cached_method def values(self): """ - Returns a list of the values of this character on each integer + Return a list of the values of this character on each integer between 0 and the modulus. EXAMPLES:: @@ -1543,88 +1538,50 @@ def values(self): sage: chi(1) 1 """ - try: - return self.__values - except AttributeError: - pass - # Build cache of all values of the Dirichlet character. - # I'm going to do it this way, since in my app the modulus - # is *always* small and we want to evaluate the character - # a *lot*. G = self.parent() R = G.base_ring() - zero = R(0) - one = R(1) - mod = self.__modulus - if self.is_trivial(): # easy special case - x = [ one ] * int(mod) - - for p in mod.prime_divisors(): - p_mult = p - while p_mult < mod: - x[p_mult] = zero - p_mult += p - if not (mod == 1): - x[0] = zero - self.__values = x - return x - - result_list = [zero] * mod - - zeta_order = G.zeta_order() - zeta = R.zeta(zeta_order) - A = rings.Integers(zeta_order) - A_zero = A.zero() - A_one = A.one() - ZZ = rings.ZZ - - S = G._integers - - R_values = G._zeta_powers + mod = self.__modulus + if mod == 1: + return [R.one()] + elif mod == 2: + return [R.zero(), R.one()] + result_list = [R.zero()] * mod gens = G.unit_gens() - last = [o - 1 for o in G.integers_mod().unit_group().gens_orders()] - - ngens = len(gens) - exponents = [0] * ngens - n = S(1) + orders = G.integers_mod().unit_group().gens_orders() - value = A_zero - val_on_gen = [ A(R_values.index(x)) for x in self.values_on_gens() ] + R_values = G._zeta_powers + val_on_gen = self.element() - final_index = ngens-1 - stop = last[-1] - while exponents[-1] <= stop: + exponents = [0] * len(orders) + n = G.integers_mod().one() + value = val_on_gen.base_ring().zero() - ######################## + while True: # record character value on n result_list[n] = R_values[value] # iterate: # increase the exponent vector by 1, # increase n accordingly, and increase value - exponents[0] += 1 # inc exponent - value += val_on_gen[0] # inc value - n *= gens[0] - ## n %= mod i = 0 - while i < final_index and exponents[i] > last[i]: + while True: + try: + exponents[i] += 1 + except IndexError: # Done! + return result_list + value += val_on_gen[i] + n *= gens[i] + if exponents[i] < orders[i]: + break exponents[i] = 0 - # now increment position i+1: - exponents[i+1] += 1 - value += val_on_gen[i+1] - n *= gens[i+1] - ## n %= mod i += 1 - self.__values = result_list - return self.__values def values_on_gens(self): - """ - Returns a tuple of the values of this character on each of the - minimal generators of `(\ZZ/N\ZZ)^*`, where - `N` is the modulus. + r""" + Return a tuple of the values of ``self`` on the standard + generators of `(\ZZ/N\ZZ)^*`, where `N` is the modulus. EXAMPLES:: @@ -2058,7 +2015,7 @@ def _coerce_in_dirichlet_character(self, x): for u in self.unit_gens(): v = u.lift() # have to do this, since e.g., unit gens mod 11 are not units mod 22. - while arith.GCD(x.modulus(),int(v)) != 1: + while x.modulus().gcd(v) != 1: v += self.modulus() a.append(R(x(v))) return self(a) diff --git a/src/sage/parallel/use_fork.py b/src/sage/parallel/use_fork.py index 18d72775b7e..ef459ffcc34 100644 --- a/src/sage/parallel/use_fork.py +++ b/src/sage/parallel/use_fork.py @@ -11,7 +11,7 @@ # http://www.gnu.org/licenses/ ################################################################################ -from sage.ext.c_lib import AlarmInterrupt +from sage.ext.interrupt import AlarmInterrupt from sage.misc.misc import alarm, cancel_alarm class p_iter_fork: diff --git a/src/sage/repl/interpreter.py b/src/sage/repl/interpreter.py index 5c0b98025e3..f86818a4c6b 100644 --- a/src/sage/repl/interpreter.py +++ b/src/sage/repl/interpreter.py @@ -755,7 +755,8 @@ def load_config_file(self, *args, **kwds): sage: from sage.misc.temporary_file import tmp_dir sage: from sage.repl.interpreter import SageTerminalApp sage: d = tmp_dir() - sage: IPYTHONDIR = os.environ['IPYTHONDIR'] + sage: from IPython.utils.path import get_ipython_dir + sage: IPYTHONDIR = get_ipython_dir() sage: os.environ['IPYTHONDIR'] = d sage: SageTerminalApp().load_config_file() sage: os.environ['IPYTHONDIR'] = IPYTHONDIR diff --git a/src/sage/repl/ipython_kernel/install.py b/src/sage/repl/ipython_kernel/install.py index e3c4cda88be..318eec7e59a 100644 --- a/src/sage/repl/ipython_kernel/install.py +++ b/src/sage/repl/ipython_kernel/install.py @@ -12,7 +12,7 @@ from IPython.kernel.kernelspec import ( get_kernel_spec, install_kernel_spec, NoSuchKernel) - +from IPython.utils.path import get_ipython_dir from sage.env import ( SAGE_ROOT, SAGE_DOC, SAGE_LOCAL, SAGE_EXTCODE, @@ -35,7 +35,7 @@ def __init__(self): 'Sage 6.6.beta2' """ self._display_name = 'Sage {0}'.format(SAGE_VERSION) - self._ipython_dir = os.environ['IPYTHONDIR'] + self._ipython_dir = get_ipython_dir() self._mkdirs() def _mkdirs(self): @@ -113,9 +113,10 @@ def use_local_mathjax(self): EXAMPLES:: sage: from sage.repl.ipython_kernel.install import SageKernelSpec + sage: from IPython.utils.path import get_ipython_dir sage: spec = SageKernelSpec() sage: spec.use_local_mathjax() - sage: ipython_dir = os.environ['IPYTHONDIR'] + sage: ipython_dir = get_ipython_dir() sage: mathjax = os.path.join(ipython_dir, 'nbextensions', 'mathjax') sage: os.path.exists(mathjax) True diff --git a/src/sage/repl/rich_output/backend_ipython.py b/src/sage/repl/rich_output/backend_ipython.py index e0889f0ac44..3b0ae9cea91 100644 --- a/src/sage/repl/rich_output/backend_ipython.py +++ b/src/sage/repl/rich_output/backend_ipython.py @@ -16,6 +16,7 @@ #***************************************************************************** import os +from IPython.display import publish_display_data from sage.repl.rich_output.backend_base import BackendBase from sage.repl.rich_output.output_catalog import * @@ -79,7 +80,36 @@ def set_underscore_variable(self, obj): """ pass - + def display_immediately(self, plain_text, rich_output): + """ + Show output immediately. + + This method is similar to the rich output :meth:`displayhook`, + except that it can be invoked at any time. + + INPUT: + + Same as :meth:`displayhook`. + + OUTPUT: + + This method does not return anything. + + EXAMPLES:: + + sage: from sage.repl.rich_output.output_basic import OutputPlainText + sage: plain_text = OutputPlainText.example() + sage: from sage.repl.rich_output.backend_ipython import BackendIPythonNotebook + sage: backend = BackendIPythonNotebook() + sage: _ = backend.display_immediately(plain_text, plain_text) + Example plain text output + """ + formatted, metadata = self.displayhook(plain_text, rich_output) + if not formatted: + return + publish_display_data(data=formatted, metadata=metadata) + + class BackendIPythonCommandline(BackendIPython): """ Backend for the IPython Command Line @@ -460,28 +490,3 @@ def displayhook(self, plain_text, rich_output): raise TypeError('rich_output type not supported') - def display_immediately(self, plain_text, rich_output): - """ - Show output immediately. - - This method is similar to the rich output :meth:`displayhook`, - except that it can be invoked at any time. - - .. TODO:: - - This does not work currently. - - INPUT/OUTPUT: - - Same as :meth:`displayhook`. - - EXAMPLES:: - - sage: from sage.repl.rich_output.output_basic import OutputPlainText - sage: plain_text = OutputPlainText.example() - sage: from sage.repl.rich_output.backend_ipython import BackendIPythonNotebook - sage: backend = BackendIPythonNotebook() - sage: backend.display_immediately(plain_text, plain_text) - ({u'text/plain': 'Example plain text output'}, {}) - """ - return self.displayhook(plain_text, rich_output) diff --git a/src/sage/rings/integer.pyx b/src/sage/rings/integer.pyx index bd09592ddd9..f7b4dbe819e 100644 --- a/src/sage/rings/integer.pyx +++ b/src/sage/rings/integer.pyx @@ -6644,7 +6644,7 @@ cdef hook_fast_tp_functions(): # Finally replace the functions called when an Integer needs # to be constructed/destructed. - hook_tp_functions(global_dummy_Integer, NULL, &fast_tp_new, NULL, &fast_tp_dealloc, False) + hook_tp_functions(global_dummy_Integer, NULL, (&fast_tp_new), NULL, &fast_tp_dealloc, False) cdef integer(x): if isinstance(x, Integer): diff --git a/src/sage/rings/invariant_theory.py b/src/sage/rings/invariant_theory.py index f8f4a0404b7..79b112456f4 100644 --- a/src/sage/rings/invariant_theory.py +++ b/src/sage/rings/invariant_theory.py @@ -2299,6 +2299,221 @@ def second(self): return self._forms[1] +###################################################################### + +class TwoTernaryQuadratics(TwoAlgebraicForms): + """ + Invariant theory of two ternary quadratics. + + You should use the :class:`invariant_theory + ` factory object to construct instances + of this class. See + :meth:`~InvariantTheoryFactory.ternary_biquadratics` for + details. + + REFERENCES: + + .. [Salmon2] + G. Salmon: A Treatise on Conic Sections, + Section on "Invariants and Covariants of Systems of Conics", + Art. 388 (a). + + TESTS:: + + sage: R. = QQ[] + sage: inv = invariant_theory.ternary_biquadratic(x^2+y^2+z^2, x*y+y*z+x*z, [x, y, z]) + sage: inv + Joint ternary quadratic with coefficients (1, 1, 1, 0, 0, 0) and ternary + quadratic with coefficients (0, 0, 0, 1, 1, 1) + sage: TestSuite(inv).run() + + sage: q1 = 73*x^2 + 96*x*y - 11*y^2 + 4*x + 63*y + 57 + sage: q2 = 61*x^2 - 100*x*y - 72*y^2 - 81*x + 39*y - 7 + sage: biquadratic = invariant_theory.ternary_biquadratic(q1, q2, [x,y]).homogenized() + sage: biquadratic._check_covariant('Delta_invariant', invariant=True) + sage: biquadratic._check_covariant('Delta_prime_invariant', invariant=True) + sage: biquadratic._check_covariant('Theta_invariant', invariant=True) + sage: biquadratic._check_covariant('Theta_prime_invariant', invariant=True) + sage: biquadratic._check_covariant('F_covariant') + sage: biquadratic._check_covariant('J_covariant') + """ + + def Delta_invariant(self): + """ + Return the `\Delta` invariant. + + EXAMPLES:: + + sage: R. = QQ[] + sage: p1 = a00*y0^2 + 2*a01*y0*y1 + a11*y1^2 + 2*a02*y0*y2 + 2*a12*y1*y2 + a22*y2^2 + sage: p2 = b00*y0^2 + 2*b01*y0*y1 + b11*y1^2 + 2*b02*y0*y2 + 2*b12*y1*y2 + b22*y2^2 + sage: q = invariant_theory.ternary_biquadratic(p1, p2, [y0, y1, y2]) + sage: coeffs = det(t * q[0].matrix() + q[1].matrix()).polynomial(t).coefficients(sparse=False) + sage: q.Delta_invariant() == coeffs[3] + True + """ + return self.get_form(0).matrix().det() + + def Delta_prime_invariant(self): + r""" + Return the `\Delta'` invariant. + + EXAMPLES:: + + sage: R. = QQ[] + sage: p1 = a00*y0^2 + 2*a01*y0*y1 + a11*y1^2 + 2*a02*y0*y2 + 2*a12*y1*y2 + a22*y2^2 + sage: p2 = b00*y0^2 + 2*b01*y0*y1 + b11*y1^2 + 2*b02*y0*y2 + 2*b12*y1*y2 + b22*y2^2 + sage: q = invariant_theory.ternary_biquadratic(p1, p2, [y0, y1, y2]) + sage: coeffs = det(t * q[0].matrix() + q[1].matrix()).polynomial(t).coefficients(sparse=False) + sage: q.Delta_prime_invariant() == coeffs[0] + True + """ + return self.get_form(1).matrix().det() + + def _Theta_helper(self, scaled_coeffs_1, scaled_coeffs_2): + """ + Internal helper method for :meth:`Theta_invariant` and + :meth:`Theta_prime_invariant`. + + TESTS:: + + sage: R. = QQ[] + sage: inv = invariant_theory.ternary_biquadratic(x^2 + y*z, x*y+z^2, x, y, z) + sage: inv._Theta_helper([1]*6, [2]*6) + 0 + """ + a00, a11, a22, a01, a02, a12 = scaled_coeffs_1 + b00, b11, b22, b01, b02, b12 = scaled_coeffs_2 + return -a12**2*b00 + a11*a22*b00 + 2*a02*a12*b01 - 2*a01*a22*b01 - \ + a02**2*b11 + a00*a22*b11 - 2*a11*a02*b02 + 2*a01*a12*b02 + \ + 2*a01*a02*b12 - 2*a00*a12*b12 - a01**2*b22 + a00*a11*b22 + + def Theta_invariant(self): + r""" + Return the `\Theta` invariant. + + EXAMPLES:: + + sage: R. = QQ[] + sage: p1 = a00*y0^2 + 2*a01*y0*y1 + a11*y1^2 + 2*a02*y0*y2 + 2*a12*y1*y2 + a22*y2^2 + sage: p2 = b00*y0^2 + 2*b01*y0*y1 + b11*y1^2 + 2*b02*y0*y2 + 2*b12*y1*y2 + b22*y2^2 + sage: q = invariant_theory.ternary_biquadratic(p1, p2, [y0, y1, y2]) + sage: coeffs = det(t * q[0].matrix() + q[1].matrix()).polynomial(t).coefficients(sparse=False) + sage: q.Theta_invariant() == coeffs[2] + True + """ + return self._Theta_helper(self.get_form(0).scaled_coeffs(), self.get_form(1).scaled_coeffs()) + + def Theta_prime_invariant(self): + r""" + Return the `\Theta'` invariant. + + EXAMPLES:: + + sage: R. = QQ[] + sage: p1 = a00*y0^2 + 2*a01*y0*y1 + a11*y1^2 + 2*a02*y0*y2 + 2*a12*y1*y2 + a22*y2^2 + sage: p2 = b00*y0^2 + 2*b01*y0*y1 + b11*y1^2 + 2*b02*y0*y2 + 2*b12*y1*y2 + b22*y2^2 + sage: q = invariant_theory.ternary_biquadratic(p1, p2, [y0, y1, y2]) + sage: coeffs = det(t * q[0].matrix() + q[1].matrix()).polynomial(t).coefficients(sparse=False) + sage: q.Theta_prime_invariant() == coeffs[1] + True + """ + return self._Theta_helper(self.get_form(1).scaled_coeffs(), self.get_form(0).scaled_coeffs()) + + def F_covariant(self): + r""" + Return the `F` covariant. + + EXAMPLES:: + + sage: R. = QQ[] + sage: p1 = 73*x^2 + 96*x*y - 11*y^2 + 4*x + 63*y + 57 + sage: p2 = 61*x^2 - 100*x*y - 72*y^2 - 81*x + 39*y - 7 + sage: q = invariant_theory.ternary_biquadratic(p1, p2, [x, y]) + sage: q.F_covariant() + -32566577*x^2 + 29060637/2*x*y + 20153633/4*y^2 - + 30250497/2*x - 241241273/4*y - 323820473/16 + """ + C = self.first().covariant_conic(self.second()) + CI = TernaryQuadratic(3, 2, C, *self.variables()) + return CI.dual().polynomial() + + def J_covariant(self): + r""" + Return the `J` covariant. + + EXAMPLES:: + + sage: R. = QQ[] + sage: p1 = 73*x^2 + 96*x*y - 11*y^2 + 4*x + 63*y + 57 + sage: p2 = 61*x^2 - 100*x*y - 72*y^2 - 81*x + 39*y - 7 + sage: q = invariant_theory.ternary_biquadratic(p1, p2, [x, y]) + sage: q.J_covariant() + 1057324024445*x^3 + 1209531088209*x^2*y + 942116599708*x*y^2 + + 984553030871*y^3 + 543715345505/2*x^2 - 3065093506021/2*x*y + + 755263948570*y^2 - 1118430692650*x - 509948695327/4*y + 3369951531745/8 + """ + return self._jacobian_determinant( + (self.first().polynomial(), 2), + (self.second().polynomial(), 2), + (self.F_covariant(), 2)) + + def syzygy(self, Delta, Theta, Theta_prime, Delta_prime, S, S_prime, F, J): + """ + Return the syzygy evaluated on the invariants and covariants. + + INPUT: + + - ``Delta``, ``Theta``, ``Theta_prime``, ``Delta_prime``, + ``S``, ``S_prime``, ``F``, ``J`` -- polynomials from the + same polynomial ring. + + OUTPUT: + + Zero if ``S`` is the first polynomial, ``S_prime`` the + second polynomial, and the remaining input are the invariants + and covariants of a ternary biquadratic. + + EXAMPLES:: + + sage: R. = QQ[] + sage: monomials = [x^2, x*y, y^2, x*z, y*z, z^2] + sage: def q_rnd(): return sum(randint(-1000,1000)*m for m in monomials) + sage: biquadratic = invariant_theory.ternary_biquadratic(q_rnd(), q_rnd(), [x,y,z]) + sage: Delta = biquadratic.Delta_invariant() + sage: Theta = biquadratic.Theta_invariant() + sage: Theta_prime = biquadratic.Theta_prime_invariant() + sage: Delta_prime = biquadratic.Delta_prime_invariant() + sage: S = biquadratic.first().polynomial() + sage: S_prime = biquadratic.second().polynomial() + sage: F = biquadratic.F_covariant() + sage: J = biquadratic.J_covariant() + sage: biquadratic.syzygy(Delta, Theta, Theta_prime, Delta_prime, S, S_prime, F, J) + 0 + + If the arguments are not the invariants and covariants then + the output is some (generically non-zero) polynomial:: + + sage: biquadratic.syzygy(1, 1, 1, 1, 1, 1, 1, x) + 1/64*x^2 + 1 + """ + R = self._ring.base_ring() + return (J**2 / R(64) + + F**3 + - 2 * F**2 * Theta*S_prime + - 2 * F**2 * Theta_prime*S + + F * S**2 * (Delta_prime * Theta + Theta_prime**2) + + F * S_prime**2 * (Delta * Theta_prime + Theta**2) + + 3 * F * S * S_prime * (Theta*Theta_prime - Delta*Delta_prime) + + S**3 * (Delta_prime**2 * Delta - Theta * Theta_prime * Delta_prime) + + S_prime**3 * (Delta**2 * Delta_prime - Theta_prime * Theta * Delta) + + S**2 * S_prime * ( + Delta_prime * Delta * Theta_prime - Theta * Theta_prime**2) + + S * S_prime**2 * ( + Delta * Delta_prime * Theta - Theta_prime * Theta**2) + ) + + ###################################################################### class TwoQuaternaryQuadratics(TwoAlgebraicForms): @@ -2566,7 +2781,7 @@ def T01(a0, a1, a2, a3, b0, b1, b2, b3, b4, b5, A0, A1, A2, A3, B0, B1, B2, B3, def T_covariant(self): """ - The $T$-covariant. + The `T`-covariant. EXAMPLES:: @@ -2588,7 +2803,7 @@ def T_covariant(self): def T_prime_covariant(self): """ - The $T'$-covariant. + The `T'`-covariant. EXAMPLES:: @@ -2611,10 +2826,10 @@ def T_prime_covariant(self): def J_covariant(self): """ - The $J$-covariant. + The `J`-covariant. This is the Jacobian determinant of the two biquadratics, the - $T$-covariant, and the $T'$-covariant with respect to the four + `T`-covariant, and the `T'`-covariant with respect to the four homogeneous variables. EXAMPLES:: @@ -3053,6 +3268,59 @@ def ternary_cubic(self, cubic, *args, **kwds): """ return TernaryCubic(3, 3, cubic, *args, **kwds) + def ternary_biquadratic(self, quadratic1, quadratic2, *args, **kwds): + """ + Invariants of two quadratics in three variables. + + INPUT: + + - ``quadratic1``, ``quadratic2`` -- two polynomials. Either + homogeneous quadratic in 3 homogeneous variables, or + inhomogeneous quadratic in 2 variables. + + - ``x``, ``y``, ``z`` -- the variables. If ``z`` is ``None``, + the quadratics are assumed to be inhomogeneous. + + EXAMPLES:: + + sage: R. = QQ[] + sage: q1 = x^2+y^2+z^2 + sage: q2 = x*y + y*z + x*z + sage: inv = invariant_theory.ternary_biquadratic(q1, q2) + sage: type(inv) + + + Distance between two circles:: + + sage: R. = QQ[] + sage: S1 = -r1^2 + x^2 + y^2 + sage: S2 = -r2^2 + (x-a)^2 + (y-b)^2 + sage: inv = invariant_theory.ternary_biquadratic(S1, S2, [x, y]) + sage: inv.Delta_invariant() + -r1^2 + sage: inv.Delta_prime_invariant() + -r2^2 + sage: inv.Theta_invariant() + a^2 + b^2 - 2*r1^2 - r2^2 + sage: inv.Theta_prime_invariant() + a^2 + b^2 - r1^2 - 2*r2^2 + sage: inv.F_covariant() + 2*x^2*a^2 + y^2*a^2 - 2*x*a^3 + a^4 + 2*x*y*a*b - 2*y*a^2*b + x^2*b^2 + + 2*y^2*b^2 - 2*x*a*b^2 + 2*a^2*b^2 - 2*y*b^3 + b^4 - 2*x^2*r1^2 - 2*y^2*r1^2 + + 2*x*a*r1^2 - 2*a^2*r1^2 + 2*y*b*r1^2 - 2*b^2*r1^2 + r1^4 - 2*x^2*r2^2 - + 2*y^2*r2^2 + 2*x*a*r2^2 - 2*a^2*r2^2 + 2*y*b*r2^2 - 2*b^2*r2^2 + 2*r1^2*r2^2 + + r2^4 + sage: inv.J_covariant() + -8*x^2*y*a^3 + 8*x*y*a^4 + 8*x^3*a^2*b - 16*x*y^2*a^2*b - 8*x^2*a^3*b + + 8*y^2*a^3*b + 16*x^2*y*a*b^2 - 8*y^3*a*b^2 + 8*x*y^2*b^3 - 8*x^2*a*b^3 + + 8*y^2*a*b^3 - 8*x*y*b^4 + 8*x*y*a^2*r1^2 - 8*y*a^3*r1^2 - 8*x^2*a*b*r1^2 + + 8*y^2*a*b*r1^2 + 8*x*a^2*b*r1^2 - 8*x*y*b^2*r1^2 - 8*y*a*b^2*r1^2 + 8*x*b^3*r1^2 - + 8*x*y*a^2*r2^2 + 8*x^2*a*b*r2^2 - 8*y^2*a*b*r2^2 + 8*x*y*b^2*r2^2 + """ + q1 = TernaryQuadratic(3, 2, quadratic1, *args, **kwds) + q2 = TernaryQuadratic(3, 2, quadratic2, *args, **kwds) + return TwoTernaryQuadratics([q1, q2]) + def quaternary_biquadratic(self, quadratic1, quadratic2, *args, **kwds): """ Invariants of two quadratics in four variables. diff --git a/src/sage/rings/laurent_series_ring.py b/src/sage/rings/laurent_series_ring.py index d58e3e079f2..fa6ff9c97a3 100644 --- a/src/sage/rings/laurent_series_ring.py +++ b/src/sage/rings/laurent_series_ring.py @@ -84,18 +84,27 @@ def LaurentSeriesRing(base_ring, name=None, names=None, default_prec=None, spars TESTS: - Check if changing global series precision does it right:: + Check if changing global series precision does it right (and + that :trac:`17955` is fixed):: sage: set_series_precision(3) sage: R. = LaurentSeriesRing(ZZ) sage: 1/(1 - 2*x) 1 + 2*x + 4*x^2 + O(x^3) + sage: set_series_precision(5) + sage: R. = LaurentSeriesRing(ZZ) + sage: 1/(1 - 2*x) + 1 + 2*x + 4*x^2 + 8*x^3 + 16*x^4 + O(x^5) sage: set_series_precision(20) """ if not names is None: name = names if name is None: raise TypeError("You must specify the name of the indeterminate of the Laurent series ring.") + if default_prec is None: + from sage.misc.defaults import series_precision + default_prec = series_precision() + global laurent_series key = (base_ring, name, default_prec, sparse) if key in laurent_series: diff --git a/src/sage/rings/polynomial/polynomial_integer_dense_ntl.pyx b/src/sage/rings/polynomial/polynomial_integer_dense_ntl.pyx index 962f094d3ad..048fa74d306 100644 --- a/src/sage/rings/polynomial/polynomial_integer_dense_ntl.pyx +++ b/src/sage/rings/polynomial/polynomial_integer_dense_ntl.pyx @@ -57,7 +57,7 @@ from sage.rings.fraction_field_element import FractionFieldElement from sage.rings.arith import lcm import sage.rings.polynomial.polynomial_ring -from sage.libs.ntl.ntl_ZZX_decl cimport *, vec_pair_ZZX_long_c +from sage.libs.ntl.ntl_ZZX_decl cimport * cdef class Polynomial_integer_dense_ntl(Polynomial): r""" diff --git a/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pxd b/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pxd index f6af12a389f..2686a6054b9 100644 --- a/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pxd +++ b/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pxd @@ -11,13 +11,9 @@ from sage.libs.ntl.ntl_lzz_pContext cimport ntl_zz_pContext_class from sage.rings.integer cimport Integer -from sage.libs.ntl.ntl_ZZ_pX_decl cimport *, ZZ_pX_c, ZZ_pX_Modulus_c -from sage.libs.ntl.ntl_lzz_pX_decl cimport *, zz_pX_c, zz_pX_Modulus_c +from sage.libs.ntl.ntl_ZZ_pX_decl cimport * +from sage.libs.ntl.ntl_lzz_pX_decl cimport * -#include "sage/libs/ntl/decl.pxi" - -#cdef extern from "ntl_wrap.h": -# struct zz_pX cdef class Polynomial_dense_mod_n(Polynomial): cdef object __poly diff --git a/src/sage/rings/real_double.pyx b/src/sage/rings/real_double.pyx index d4fdec4b3d2..cb69ac4c4aa 100644 --- a/src/sage/rings/real_double.pyx +++ b/src/sage/rings/real_double.pyx @@ -2745,7 +2745,7 @@ cdef void fast_tp_dealloc(PyObject* o): from sage.misc.allocator cimport hook_tp_functions -hook_tp_functions(global_dummy_element, NULL, &fast_tp_new, NULL, &fast_tp_dealloc, False) +hook_tp_functions(global_dummy_element, NULL, (&fast_tp_new), NULL, &fast_tp_dealloc, False) def time_alloc_list(n): diff --git a/src/sage/structure/element.pyx b/src/sage/structure/element.pyx index 48592801a53..112cead33a8 100644 --- a/src/sage/structure/element.pyx +++ b/src/sage/structure/element.pyx @@ -147,9 +147,9 @@ be accessible from the calling function after this operation. ################################################################## include "sage/ext/cdefs.pxi" -include "sage/ext/stdsage.pxi" include "sage/ext/python.pxi" include "coerce.pxi" +from sage.ext.stdsage cimport * from cpython.ref cimport PyObject diff --git a/src/sage/tensor/all.py b/src/sage/tensor/all.py index 3567cc0428c..4cb8a34eb8a 100644 --- a/src/sage/tensor/all.py +++ b/src/sage/tensor/all.py @@ -1,3 +1,4 @@ from coordinate_patch import CoordinatePatch from differential_forms import DifferentialForms from differential_form_element import DifferentialForm, wedge +from modules.all import * diff --git a/src/sage/tensor/modules/__init__.py b/src/sage/tensor/modules/__init__.py new file mode 100644 index 00000000000..932b79829cf --- /dev/null +++ b/src/sage/tensor/modules/__init__.py @@ -0,0 +1 @@ +# Empty file diff --git a/src/sage/tensor/modules/all.py b/src/sage/tensor/modules/all.py new file mode 100644 index 00000000000..a6d8750ae90 --- /dev/null +++ b/src/sage/tensor/modules/all.py @@ -0,0 +1 @@ +from finite_rank_free_module import FiniteRankFreeModule diff --git a/src/sage/tensor/modules/comp.py b/src/sage/tensor/modules/comp.py new file mode 100644 index 00000000000..ef44f8a7145 --- /dev/null +++ b/src/sage/tensor/modules/comp.py @@ -0,0 +1,4651 @@ +r""" +Components as indexed sets of ring elements + +The class :class:`Components` is a technical class to take in charge the +storage and manipulation of **indexed elements of a commutative ring** that +represent the components of some "mathematical entity" with respect to some +"frame". Examples of *entity/frame* are *vector/vector-space basis* or +*vector field/vector frame on some manifold*. More generally, the components +can be those of a tensor on a free module or those of a tensor field on a +manifold. They can also be non-tensorial quantities, like connection +coefficients or structure coefficients of a vector frame. + +The individual components are assumed to belong to a given commutative ring +and are labelled by *indices*, which are *tuples of integers*. +The following operations are implemented on components with respect +to a given frame: + +* arithmetics (addition, subtraction, multiplication by a ring element) +* handling of symmetries or antisymmetries on the indices +* symmetrization and antisymmetrization +* tensor product +* contraction + +Various subclasses of class :class:`Components` are + +* :class:`CompWithSym` for components with symmetries or antisymmetries w.r.t. + index permutations + + * :class:`CompFullySym` for fully symmetric components w.r.t. index + permutations + + * :class:`KroneckerDelta` for the Kronecker delta symbol + + * :class:`CompFullyAntiSym` for fully antisymmetric components w.r.t. index + permutations + +AUTHORS: + +- Eric Gourgoulhon, Michal Bejger (2014-2015): initial version +- Joris Vankerschaver (2010): for the idea of storing only the non-zero + components as dictionaries, whose keys are the component indices (see + class :class:`~sage.tensor.differential_form_element.DifferentialForm`) + +EXAMPLES: + +Set of components with 2 indices on a 3-dimensional vector space, the frame +being some basis of the vector space:: + + sage: from sage.tensor.modules.comp import Components + sage: V = VectorSpace(QQ,3) + sage: basis = V.basis() ; basis + [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: c = Components(QQ, basis, 2) ; c + 2-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + +Actually, the frame can be any object that has some length, i.e. on which +the function :func:`len()` can be called:: + + sage: basis1 = V.gens() ; basis1 + ((1, 0, 0), (0, 1, 0), (0, 0, 1)) + sage: c1 = Components(QQ, basis1, 2) ; c1 + 2-indices components w.r.t. ((1, 0, 0), (0, 1, 0), (0, 0, 1)) + sage: basis2 = ['a', 'b' , 'c'] + sage: c2 = Components(QQ, basis2, 2) ; c2 + 2-indices components w.r.t. ['a', 'b', 'c'] + +A just created set of components is initialized to zero:: + + sage: c.is_zero() + True + sage: c == 0 + True + +This can also be checked on the list of components, which is returned by +the operator ``[:]``:: + + sage: c[:] + [0 0 0] + [0 0 0] + [0 0 0] + +Individual components are accessed by providing their indices inside +square brackets:: + + sage: c[1,2] = -3 + sage: c[:] + [ 0 0 0] + [ 0 0 -3] + [ 0 0 0] + sage: v = Components(QQ, basis, 1) + sage: v[:] + [0, 0, 0] + sage: v[0] + 0 + sage: v[:] = (-1,3,2) + sage: v[:] + [-1, 3, 2] + sage: v[0] + -1 + +Sets of components with 2 indices can be converted into a matrix:: + + sage: matrix(c) + [ 0 0 0] + [ 0 0 -3] + [ 0 0 0] + sage: matrix(c).parent() + Full MatrixSpace of 3 by 3 dense matrices over Rational Field + +By default, the indices range from `0` to `n-1`, where `n` is the length +of the frame. This can be changed via the argument ``start_index`` in +the :class:`Components` constructor:: + + sage: v1 = Components(QQ, basis, 1, start_index=1) + sage: v1[:] + [0, 0, 0] + sage: v1[0] + Traceback (most recent call last): + ... + IndexError: index out of range: 0 not in [1, 3] + sage: v1[1] + 0 + sage: v1[:] = v[:] # list copy of all components + sage: v1[:] + [-1, 3, 2] + sage: v1[1], v1[2], v1[3] + (-1, 3, 2) + sage: v[0], v[1], v[2] + (-1, 3, 2) + +If some formatter function or unbound method is provided via the argument +``output_formatter`` in the :class:`Components` constructor, it is used to +change the ouput of the access operator ``[...]``:: + + sage: a = Components(QQ, basis, 2, output_formatter=Rational.numerical_approx) + sage: a[1,2] = 1/3 + sage: a[1,2] + 0.333333333333333 + +The format can be passed to the formatter as the last argument of the +access operator ``[...]``:: + + sage: a[1,2,10] # here the format is 10, for 10 bits of precision + 0.33 + sage: a[1,2,100] + 0.33333333333333333333333333333 + +The raw (unformatted) components are then accessed by the double bracket +operator:: + + sage: a[[1,2]] + 1/3 + +For sets of components declared without any output formatter, there is no +difference between ``[...]`` and ``[[...]]``:: + + sage: c[1,2] = 1/3 + sage: c[1,2], c[[1,2]] + (1/3, 1/3) + +The formatter is also used for the complete list of components:: + + sage: a[:] + [0.000000000000000 0.000000000000000 0.000000000000000] + [0.000000000000000 0.000000000000000 0.333333333333333] + [0.000000000000000 0.000000000000000 0.000000000000000] + sage: a[:,10] # with a format different from the default one (53 bits) + [0.00 0.00 0.00] + [0.00 0.00 0.33] + [0.00 0.00 0.00] + +The complete list of components in raw form can be recovered by the double +bracket operator, replacing ``:`` by ``slice(None)`` (since ``a[[:]]`` +generates a Python syntax error):: + + sage: a[[slice(None)]] + [ 0 0 0] + [ 0 0 1/3] + [ 0 0 0] + +Another example of formatter: the Python built-in function :func:`str` +to generate string outputs:: + + sage: b = Components(QQ, V.basis(), 1, output_formatter=str) + sage: b[:] = (1, 0, -4) + sage: b[:] + ['1', '0', '-4'] + +For such a formatter, 2-indices components are no longer displayed as a +matrix:: + + sage: b = Components(QQ, basis, 2, output_formatter=str) + sage: b[0,1] = 1/3 + sage: b[:] + [['0', '1/3', '0'], ['0', '0', '0'], ['0', '0', '0']] + +But unformatted outputs still are:: + + sage: b[[slice(None)]] + [ 0 1/3 0] + [ 0 0 0] + [ 0 0 0] + +Internally, the components are stored as a dictionary (:attr:`_comp`) whose +keys are the indices; only the non-zero components are stored:: + + sage: a[:] + [0.000000000000000 0.000000000000000 0.000000000000000] + [0.000000000000000 0.000000000000000 0.333333333333333] + [0.000000000000000 0.000000000000000 0.000000000000000] + sage: a._comp + {(1, 2): 1/3} + sage: v[:] = (-1, 0, 3) + sage: v._comp # random output order of the component dictionary + {(0,): -1, (2,): 3} + +In case of symmetries, only non-redundant components are stored:: + + sage: from sage.tensor.modules.comp import CompFullyAntiSym + sage: c = CompFullyAntiSym(QQ, basis, 2) + sage: c[0,1] = 3 + sage: c[:] + [ 0 3 0] + [-3 0 0] + [ 0 0 0] + sage: c._comp + {(0, 1): 3} + +""" + +#****************************************************************************** +# Copyright (C) 2015 Eric Gourgoulhon +# Copyright (C) 2015 Michal Bejger +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# http://www.gnu.org/licenses/ +#****************************************************************************** + +from sage.structure.sage_object import SageObject +from sage.rings.integer import Integer + +class Components(SageObject): + r""" + Indexed set of ring elements forming some components with respect + to a given "frame". + + The "frame" can be a basis of some vector space or a vector frame on some + manifold (i.e. a field of bases). + The stored quantities can be tensor components or non-tensorial quantities, + such as connection coefficients or structure coefficents. The symmetries + over some indices are dealt by subclasses of the class :class:`Components`. + + INPUT: + + - ``ring`` -- commutative ring in which each component takes its value + - ``frame`` -- frame with respect to which the components are defined; + whatever type ``frame`` is, it should have a method ``__len__()`` + implemented, so that ``len(frame)`` returns the dimension, i.e. the size + of a single index range + - ``nb_indices`` -- number of integer indices labeling the components + - ``start_index`` -- (default: 0) first value of a single index; + accordingly a component index i must obey + ``start_index <= i <= start_index + dim - 1``, where ``dim = len(frame)``. + - ``output_formatter`` -- (default: ``None``) function or unbound + method called to format the output of the component access + operator ``[...]`` (method __getitem__); ``output_formatter`` must take + 1 or 2 arguments: the 1st argument must be an element of ``ring`` and + the second one, if any, some format specification. + + EXAMPLES: + + Set of components with 2 indices on a 3-dimensional vector space, the frame + being some basis of the vector space:: + + sage: from sage.tensor.modules.comp import Components + sage: V = VectorSpace(QQ,3) + sage: basis = V.basis() ; basis + [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: c = Components(QQ, basis, 2) ; c + 2-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + + Actually, the frame can be any object that has some length, i.e. on which + the function :func:`len()` can be called:: + + sage: basis1 = V.gens() ; basis1 + ((1, 0, 0), (0, 1, 0), (0, 0, 1)) + sage: c1 = Components(QQ, basis1, 2) ; c1 + 2-indices components w.r.t. ((1, 0, 0), (0, 1, 0), (0, 0, 1)) + sage: basis2 = ['a', 'b' , 'c'] + sage: c2 = Components(QQ, basis2, 2) ; c2 + 2-indices components w.r.t. ['a', 'b', 'c'] + + By default, the indices range from `0` to `n-1`, where `n` is the length + of the frame. This can be changed via the argument ``start_index``:: + + sage: c1 = Components(QQ, basis, 2, start_index=1) + sage: c1[0,1] + Traceback (most recent call last): + ... + IndexError: index out of range: 0 not in [1, 3] + sage: c[0,1] # for c, the index 0 is OK + 0 + sage: c[0,1] = -3 + sage: c1[:] = c[:] # list copy of all components + sage: c1[1,2] # (1,2) = (0,1) shifted by 1 + -3 + + If some formatter function or unbound method is provided via the argument + ``output_formatter``, it is used to change the ouput of the access + operator ``[...]``:: + + sage: a = Components(QQ, basis, 2, output_formatter=Rational.numerical_approx) + sage: a[1,2] = 1/3 + sage: a[1,2] + 0.333333333333333 + + The format can be passed to the formatter as the last argument of the + access operator ``[...]``:: + + sage: a[1,2,10] # here the format is 10, for 10 bits of precision + 0.33 + sage: a[1,2,100] + 0.33333333333333333333333333333 + + The raw (unformatted) components are then accessed by the double bracket + operator:: + + sage: a[[1,2]] + 1/3 + + For sets of components declared without any output formatter, there is no + difference between ``[...]`` and ``[[...]]``:: + + sage: c[1,2] = 1/3 + sage: c[1,2], c[[1,2]] + (1/3, 1/3) + + The formatter is also used for the complete list of components:: + + sage: a[:] + [0.000000000000000 0.000000000000000 0.000000000000000] + [0.000000000000000 0.000000000000000 0.333333333333333] + [0.000000000000000 0.000000000000000 0.000000000000000] + sage: a[:,10] # with a format different from the default one (53 bits) + [0.00 0.00 0.00] + [0.00 0.00 0.33] + [0.00 0.00 0.00] + + The complete list of components in raw form can be recovered by the double + bracket operator, replacing ``:`` by ``slice(None)`` (since ``a[[:]]`` + generates a Python syntax error):: + + sage: a[[slice(None)]] + [ 0 0 0] + [ 0 0 1/3] + [ 0 0 0] + + Another example of formatter: the Python built-in function :func:`str` + to generate string outputs:: + + sage: b = Components(QQ, V.basis(), 1, output_formatter=str) + sage: b[:] = (1, 0, -4) + sage: b[:] + ['1', '0', '-4'] + + For such a formatter, 2-indices components are no longer displayed as a + matrix:: + + sage: b = Components(QQ, basis, 2, output_formatter=str) + sage: b[0,1] = 1/3 + sage: b[:] + [['0', '1/3', '0'], ['0', '0', '0'], ['0', '0', '0']] + + But unformatted outputs still are:: + + sage: b[[slice(None)]] + [ 0 1/3 0] + [ 0 0 0] + [ 0 0 0] + + Internally, the components are stored as a dictionary (:attr:`_comp`) whose + keys are the indices; only the non-zero components are stored:: + + sage: a[:] + [0.000000000000000 0.000000000000000 0.000000000000000] + [0.000000000000000 0.000000000000000 0.333333333333333] + [0.000000000000000 0.000000000000000 0.000000000000000] + sage: a._comp + {(1, 2): 1/3} + sage: v = Components(QQ, basis, 1) + sage: v[:] = (-1, 0, 3) + sage: v._comp # random output order of the component dictionary + {(0,): -1, (2,): 3} + + + .. RUBRIC:: ARITHMETIC EXAMPLES: + + Unary plus operator:: + + sage: a = Components(QQ, basis, 1) + sage: a[:] = (-1, 0, 3) + sage: s = +a ; s[:] + [-1, 0, 3] + sage: +a == a + True + + Unary minus operator:: + + sage: s = -a ; s[:] + [1, 0, -3] + + Addition:: + + sage: b = Components(QQ, basis, 1) + sage: b[:] = (2, 1, 4) + sage: s = a + b ; s[:] + [1, 1, 7] + sage: a + b == b + a + True + sage: a + (-a) == 0 + True + + Subtraction:: + + sage: s = a - b ; s[:] + [-3, -1, -1] + sage: s + b == a + True + sage: a - b == - (b - a) + True + + Multiplication by a scalar:: + + sage: s = 2*a ; s[:] + [-2, 0, 6] + + Division by a scalar:: + + sage: s = a/2 ; s[:] + [-1/2, 0, 3/2] + sage: 2*(a/2) == a + True + + Tensor product (by means of the operator ``*``):: + + sage: c = a*b ; c + 2-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: a[:], b[:] + ([-1, 0, 3], [2, 1, 4]) + sage: c[:] + [-2 -1 -4] + [ 0 0 0] + [ 6 3 12] + sage: d = c*a ; d + 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: d[:] + [[[2, 0, -6], [1, 0, -3], [4, 0, -12]], + [[0, 0, 0], [0, 0, 0], [0, 0, 0]], + [[-6, 0, 18], [-3, 0, 9], [-12, 0, 36]]] + sage: d[0,1,2] == a[0]*b[1]*a[2] + True + + """ + def __init__(self, ring, frame, nb_indices, start_index=0, + output_formatter=None): + r""" + TEST:: + + sage: from sage.tensor.modules.comp import Components + sage: Components(ZZ, [1,2,3], 2) + 2-indices components w.r.t. [1, 2, 3] + + """ + # For efficiency, no test is performed regarding the type and range of + # the arguments: + self._ring = ring + self._frame = frame + self._nid = nb_indices + self._dim = len(frame) + self._sindex = start_index + self._output_formatter = output_formatter + self._comp = {} # the dictionary of components, with the index tuples + # as keys + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLE:: + + sage: from sage.tensor.modules.comp import Components + sage: c = Components(ZZ, [1,2,3], 2) + sage: c._repr_() + '2-indices components w.r.t. [1, 2, 3]' + + """ + description = str(self._nid) + if self._nid == 1: + description += "-index" + else: + description += "-indices" + description += " components w.r.t. " + str(self._frame) + return description + + def _new_instance(self): + r""" + Creates a :class:`Components` instance of the same number of indices + and w.r.t. the same frame. + + This method must be redefined by derived classes of + :class:`Components`. + + EXAMPLE:: + + sage: from sage.tensor.modules.comp import Components + sage: c = Components(ZZ, [1,2,3], 2) + sage: c._new_instance() + 2-indices components w.r.t. [1, 2, 3] + + """ + return Components(self._ring, self._frame, self._nid, self._sindex, + self._output_formatter) + + def copy(self): + r""" + Return an exact copy of ``self``. + + EXAMPLES: + + Copy of a set of components with a single index:: + + sage: from sage.tensor.modules.comp import Components + sage: V = VectorSpace(QQ,3) + sage: a = Components(QQ, V.basis(), 1) + sage: a[:] = -2, 1, 5 + sage: b = a.copy() ; b + 1-index components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: b[:] + [-2, 1, 5] + sage: b == a + True + sage: b is a # b is a distinct object + False + + """ + result = self._new_instance() + for ind, val in self._comp.iteritems(): + if hasattr(val, 'copy'): + result._comp[ind] = val.copy() + else: + result._comp[ind] = val + return result + + def _del_zeros(self): + r""" + Deletes all the zeros in the dictionary :attr:`_comp` + + NB: The use case of this method must be rare because zeros are not + stored in :attr:`_comp`. + + EXAMPLE:: + + sage: from sage.tensor.modules.comp import Components + sage: c = Components(ZZ, [1,2,3], 2) + sage: c._comp = {(0,1): 3, (0,2): 0, (1,2): -5, (2,2): 0} # enforcing zero storage + sage: c._del_zeros() + sage: c._comp + {(0, 1): 3, (1, 2): -5} + + """ + # The zeros are first searched; they are deleted in a second stage, to + # avoid changing the dictionary while it is read + zeros = [] + for ind, value in self._comp.iteritems(): + if value == 0: + zeros.append(ind) + for ind in zeros: + del self._comp[ind] + + def _check_indices(self, indices): + r""" + Check the validity of a list of indices and returns a tuple from it + + INPUT: + + - ``indices`` -- list of indices (possibly a single integer if + self is a 1-index object) + + OUTPUT: + + - a tuple containing valid indices + + EXAMPLES:: + + sage: from sage.tensor.modules.comp import Components + sage: c = Components(ZZ, [1,2,3], 2) + sage: c._check_indices((0,1)) + (0, 1) + sage: c._check_indices([0,1]) + (0, 1) + sage: c._check_indices([2,1]) + (2, 1) + sage: c._check_indices([2,3]) + Traceback (most recent call last): + ... + IndexError: index out of range: 3 not in [0, 2] + sage: c._check_indices(1) + Traceback (most recent call last): + ... + ValueError: wrong number of indices: 2 expected, while 1 are provided + sage: c._check_indices([1,2,3]) + Traceback (most recent call last): + ... + ValueError: wrong number of indices: 2 expected, while 3 are provided + + """ + if isinstance(indices, (int, Integer)): + ind = (indices,) + else: + ind = tuple(indices) + if len(ind) != self._nid: + raise ValueError(("wrong number of indices: {} expected," + " while {} are provided").format(self._nid, len(ind))) + si = self._sindex + imax = self._dim - 1 + si + for k in range(self._nid): + i = ind[k] + if i < si or i > imax: + raise IndexError("index out of range: " + + "{} not in [{}, {}]".format(i, si, imax)) + return ind + + def __getitem__(self, args): + r""" + Returns the component corresponding to the given indices. + + INPUT: + + - ``args`` -- list of indices (possibly a single integer if + self is a 1-index object) or the character ``:`` for the full list + of components + + OUTPUT: + + - the component corresponding to ``args`` or, if ``args`` = ``:``, + the full list of components, in the form ``T[i][j]...`` for the + components `T_{ij...}` (for a 2-indices object, a matrix is returned) + + EXAMPLES:: + + sage: from sage.tensor.modules.comp import Components + sage: c = Components(ZZ, [1,2,3], 2) + sage: c[1,2] # unset components are zero + 0 + sage: c.__getitem__((1,2)) + 0 + sage: c.__getitem__([1,2]) + 0 + sage: c[1,2] = -4 + sage: c[1,2] + -4 + sage: c.__getitem__((1,2)) + -4 + sage: c[:] + [ 0 0 0] + [ 0 0 -4] + [ 0 0 0] + sage: c.__getitem__(slice(None)) + [ 0 0 0] + [ 0 0 -4] + [ 0 0 0] + + """ + no_format = self._output_formatter is None + format_type = None # default value, possibly redefined below + if isinstance(args, list): # case of [[...]] syntax + no_format = True + if isinstance(args[0], slice): + indices = args[0] + elif isinstance(args[0], (tuple, list)): # to ensure equivalence between + indices = args[0] # [[(i,j,...)]] or [[[i,j,...]]] and [[i,j,...]] + else: + indices = tuple(args) + else: + # Determining from the input the list of indices and the format + if isinstance(args, (int, Integer, slice)): + indices = args + elif isinstance(args[0], slice): + indices = args[0] + if len(args) == 2: + format_type = args[1] + elif len(args) == self._nid: + indices = args + else: + format_type = args[-1] + indices = args[:-1] + if isinstance(indices, slice): + return self._get_list(indices, no_format, format_type) + else: + ind = self._check_indices(indices) + if ind in self._comp: + if no_format: + return self._comp[ind] + elif format_type is None: + return self._output_formatter(self._comp[ind]) + else: + return self._output_formatter(self._comp[ind], format_type) + else: # if the value is not stored in self._comp, it is zero: + if no_format: + return self._ring.zero() + elif format_type is None: + return self._output_formatter(self._ring.zero()) + else: + return self._output_formatter(self._ring.zero(), + format_type) + + def _get_list(self, ind_slice, no_format=True, format_type=None): + r""" + Return the list of components. + + INPUT: + + - ``ind_slice`` -- a slice object + + OUTPUT: + + - the full list of components if ``ind_slice = [:]``, or a slice + of it if ``ind_slice = [a:b]`` (1-D case), in the form + ``T[i][j]...`` for the components `T_{ij...}` (for a 2-indices + object, a matrix is returned). + + EXAMPLES:: + + sage: from sage.tensor.modules.comp import Components + sage: c = Components(ZZ, [1,2,3], 2) + sage: c[0,1], c[1,2] = 5, -4 + sage: c._get_list(slice(None)) + [ 0 5 0] + [ 0 0 -4] + [ 0 0 0] + sage: v = Components(ZZ, [1,2,3], 1) + sage: v[:] = 4, 5, 6 + sage: v._get_list(slice(None)) + [4, 5, 6] + sage: v._get_list(slice(0,1)) + [4] + sage: v._get_list(slice(0,2)) + [4, 5] + sage: v._get_list(slice(2,3)) + [6] + + """ + from sage.matrix.constructor import matrix + si = self._sindex + nsi = si + self._dim + if self._nid == 1: + if ind_slice.start is None: + start = si + else: + start = ind_slice.start + if ind_slice.stop is None: + stop = nsi + else: + stop = ind_slice.stop + if ind_slice.step is not None: + raise NotImplementedError("function [start:stop:step] not implemented") + if no_format: + return [self[[i]] for i in range(start, stop)] + else: + return [self[i, format_type] for i in range(start, stop)] + if ind_slice.start is not None or ind_slice.stop is not None: + raise NotImplementedError("function [start:stop] not " + + "implemented for components with {} indices".format(self._nid)) + resu = [self._gen_list([i], no_format, format_type) + for i in range(si, nsi)] + if self._nid == 2: + try: + for i in range(self._dim): + for j in range(self._dim): + a = resu[i][j] + if hasattr(a, '_express'): + resu[i][j] = a._express + resu = matrix(resu) # for a nicer output + except TypeError: + pass + return resu + + def _gen_list(self, ind, no_format=True, format_type=None): + r""" + Recursive function to generate the list of values. + + EXAMPLES:: + + sage: from sage.tensor.modules.comp import Components + sage: c = Components(ZZ, [1,2,3], 2) + sage: c[0,1], c[1,2] = 5, -4 + sage: c._gen_list([]) + [[0, 5, 0], [0, 0, -4], [0, 0, 0]] + sage: c._gen_list([0]) + [0, 5, 0] + sage: c._gen_list([1]) + [0, 0, -4] + sage: c._gen_list([2]) + [0, 0, 0] + sage: c._gen_list([0,1]) + 5 + + """ + if len(ind) == self._nid: + if no_format: + return self[ind] + else: + args = tuple(ind + [format_type]) + return self.__getitem__(args) + else: + si = self._sindex + nsi = si + self._dim + return [self._gen_list(ind + [i], no_format, format_type) + for i in range(si, nsi)] + + def __setitem__(self, args, value): + r""" + Sets the component corresponding to the given indices. + + INPUT: + + - ``args`` -- list of indices (possibly a single integer if + self is a 1-index object); if ``[:]`` is provided, all the + components are set + - ``value`` -- the value to be set or a list of values if + ``args = [:]`` (``slice(None)``) + + EXAMPLES:: + + sage: from sage.tensor.modules.comp import Components + sage: c = Components(ZZ, [1,2,3], 2) + sage: c.__setitem__((0,1), -4) + sage: c[:] + [ 0 -4 0] + [ 0 0 0] + [ 0 0 0] + sage: c[0,1] = -4 + sage: c[:] + [ 0 -4 0] + [ 0 0 0] + [ 0 0 0] + sage: c.__setitem__(slice(None), [[0, 1, 2], [3, 4, 5], [6, 7, 8]]) + sage: c[:] + [0 1 2] + [3 4 5] + [6 7 8] + + """ + format_type = None # default value, possibly redefined below + if isinstance(args, list): # case of [[...]] syntax + if isinstance(args[0], slice): + indices = args[0] + elif isinstance(args[0], (tuple, list)): # to ensure equivalence between + indices = args[0] # [[(i,j,...)]] or [[[i,j,...]]] and [[i,j,...]] + else: + indices = tuple(args) + else: + # Determining from the input the list of indices and the format + if isinstance(args, (int, Integer, slice)): + indices = args + elif isinstance(args[0], slice): + indices = args[0] + if len(args) == 2: + format_type = args[1] + elif len(args) == self._nid: + indices = args + else: + format_type = args[-1] + indices = args[:-1] + if isinstance(indices, slice): + self._set_list(indices, format_type, value) + else: + ind = self._check_indices(indices) + if value == 0: + # if the component has been set previously, it is deleted, + # otherwise nothing is done: + if ind in self._comp: + del self._comp[ind] + else: + if format_type is None: + self._comp[ind] = self._ring(value) + else: + self._comp[ind] = self._ring({format_type: value}) + # NB: the writing + # self._comp[ind] = self._ring(value, format_type) + # is not allowed when ring is an algebra and value some + # element of the algebra's base ring, cf. the discussion at + # http://trac.sagemath.org/ticket/16054 + + def _set_list(self, ind_slice, format_type, values): + r""" + Set the components from a list. + + INPUT: + + - ``ind_slice`` -- a slice object + - ``format_type`` -- format possibly used to construct a ring element + - ``values`` -- list of values for the components : the full list if + ``ind_slice = [:]``, in the form ``T[i][j]...`` for the + component `T_{ij...}`; in the 1-D case, ``ind_slice`` can be + a slice of the full list, in the form ``[a:b]`` + + EXAMPLE:: + + sage: from sage.tensor.modules.comp import Components + sage: c = Components(ZZ, [1,2,3], 2) + sage: c._set_list(slice(None), None, [[0, 1, 2], [3, 4, 5], [6, 7, 8]]) + sage: c[:] + [0 1 2] + [3 4 5] + [6 7 8] + + """ + si = self._sindex + nsi = si + self._dim + if self._nid == 1: + if ind_slice.start is None: + start = si + else: + start = ind_slice.start + if ind_slice.stop is None: + stop = nsi + else: + stop = ind_slice.stop + if ind_slice.step is not None: + raise NotImplementedError("function [start:stop:step] not implemented") + for i in range(start, stop): + self[i, format_type] = values[i-start] + else: + if ind_slice.start is not None or ind_slice.stop is not None: + raise NotImplementedError("function [start:stop] not " + + "implemented for components with {} indices".format(self._nid)) + for i in range(si, nsi): + self._set_value_list([i], format_type, values[i-si]) + + def _set_value_list(self, ind, format_type, val): + r""" + Recursive function to set a list of values to ``self``. + + EXAMPLE:: + + sage: from sage.tensor.modules.comp import Components + sage: c = Components(ZZ, [1,2,3], 2) + sage: c._set_value_list([], None, [[1,2,3], [4,5,6], [7,8,9]]) + sage: c[:] + [1 2 3] + [4 5 6] + [7 8 9] + sage: c._set_value_list([0], None, [-1,-2,-3]) + sage: c[:] + [-1 -2 -3] + [ 4 5 6] + [ 7 8 9] + sage: c._set_value_list([2,1], None, -8) + sage: c[:] + [-1 -2 -3] + [ 4 5 6] + [ 7 -8 9] + + """ + if len(ind) == self._nid: + if format_type is not None: + ind = tuple(ind + [format_type]) + self[ind] = val + else: + si = self._sindex + nsi = si + self._dim + for i in range(si, nsi): + self._set_value_list(ind + [i], format_type, val[i-si]) + + def swap_adjacent_indices(self, pos1, pos2, pos3): + r""" + Swap two adjacent sets of indices. + + This method is essentially required to reorder the covariant and + contravariant indices in the computation of a tensor product. + + INPUT: + + - ``pos1`` -- position of the first index of set 1 (with the convention + ``position=0`` for the first slot) + - ``pos2`` -- position of the first index of set 2 equals 1 plus the + position of the last index of set 1 (since the two sets are adjacent) + - ``pos3`` -- 1 plus position of the last index of set 2 + + OUTPUT: + + - Components with index set 1 permuted with index set 2. + + EXAMPLES: + + Swap of the two indices of a 2-indices set of components:: + + sage: from sage.tensor.modules.comp import Components + sage: V = VectorSpace(QQ, 3) + sage: c = Components(QQ, V.basis(), 2) + sage: c[:] = [[1,2,3], [4,5,6], [7,8,9]] + sage: c1 = c.swap_adjacent_indices(0,1,2) + sage: c[:], c1[:] + ( + [1 2 3] [1 4 7] + [4 5 6] [2 5 8] + [7 8 9], [3 6 9] + ) + + Swap of two pairs of indices on a 4-indices set of components:: + + sage: d = c*c1 ; d + 4-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: d1 = d.swap_adjacent_indices(0,2,4) + sage: d[0,1,1,2] + 16 + sage: d1[1,2,0,1] + 16 + sage: d1[0,1,1,2] + 24 + sage: d[1,2,0,1] + 24 + + """ + result = self._new_instance() + for ind, val in self._comp.iteritems(): + new_ind = ind[:pos1] + ind[pos2:pos3] + ind[pos1:pos2] + ind[pos3:] + result._comp[new_ind] = val + # the above writing is more efficient than result[new_ind] = val + # it does not work for the derived class CompWithSym, but for the + # latter, the function CompWithSym.swap_adjacent_indices will be + # called and not the present function. + return result + + def is_zero(self): + r""" + Return ``True`` if all the components are zero and ``False`` otherwise. + + EXAMPLES: + + A just-created set of components is initialized to zero:: + + sage: from sage.tensor.modules.comp import Components + sage: V = VectorSpace(QQ,3) + sage: c = Components(QQ, V.basis(), 1) + sage: c.is_zero() + True + sage: c[:] + [0, 0, 0] + sage: c[0] = 1 ; c[:] + [1, 0, 0] + sage: c.is_zero() + False + sage: c[0] = 0 ; c[:] + [0, 0, 0] + sage: c.is_zero() + True + + It is equivalent to use the operator == to compare to zero:: + + sage: c == 0 + True + sage: c != 0 + False + + Comparing to a nonzero number is meaningless:: + + sage: c == 1 + Traceback (most recent call last): + ... + TypeError: cannot compare a set of components to a number + + """ + if not self._comp: + return True + + #!# What follows could be skipped since _comp should not contain + # any zero value + # In other words, the full method should be + # return self.comp == {} + for val in self._comp.itervalues(): + if val != 0: + return False + return True + + def __eq__(self, other): + r""" + Comparison (equality) operator. + + INPUT: + + - ``other`` -- a set of components or 0 + + OUTPUT: + + - ``True`` if ``self`` is equal to ``other``, or ``False`` otherwise + + EXAMPLES:: + + sage: from sage.tensor.modules.comp import Components + sage: c = Components(ZZ, [1,2,3], 2) + sage: c.__eq__(0) # uninitialized components are zero + True + sage: c[0,1], c[1,2] = 5, -4 + sage: c.__eq__(0) + False + sage: c1 = Components(ZZ, [1,2,3], 2) + sage: c1[0,1] = 5 + sage: c.__eq__(c1) + False + sage: c1[1,2] = -4 + sage: c.__eq__(c1) + True + sage: v = Components(ZZ, [1,2,3], 1) + sage: c.__eq__(v) + False + + """ + if isinstance(other, (int, Integer)): # other is 0 + if other == 0: + return self.is_zero() + else: + raise TypeError("cannot compare a set of components to a number") + else: # other is another Components + if not isinstance(other, Components): + raise TypeError("an instance of Components is expected") + if other._frame != self._frame: + return False + if other._nid != self._nid: + return False + if other._sindex != self._sindex: + return False + if other._output_formatter != self._output_formatter: + return False + return (self - other).is_zero() + + def __ne__(self, other): + r""" + Non-equality operator. + + INPUT: + + - ``other`` -- a set of components or 0 + + OUTPUT: + + - True if ``self`` is different from ``other``, or False otherwise + + EXAMPLES:: + + sage: from sage.tensor.modules.comp import Components + sage: c = Components(ZZ, [1,2,3], 1) + sage: c.__ne__(0) # uninitialized components are zero + False + sage: c1 = Components(ZZ, [1,2,3], 1) + sage: c.__ne__(c1) # c and c1 are both zero + False + sage: c[0] = 4 + sage: c.__ne__(0) + True + sage: c.__ne__(c1) + True + + """ + return not self.__eq__(other) + + def __pos__(self): + r""" + Unary plus operator. + + OUTPUT: + + - an exact copy of ``self`` + + EXAMPLE:: + + sage: from sage.tensor.modules.comp import Components + sage: c = Components(ZZ, [1,2,3], 1) + sage: c[:] = 5, 0, -4 + sage: a = c.__pos__() ; a + 1-index components w.r.t. [1, 2, 3] + sage: a[:] + [5, 0, -4] + sage: a == +c + True + sage: a == c + True + + """ + return self.copy() + + def __neg__(self): + r""" + Unary minus operator. + + OUTPUT: + + - the opposite of the components represented by ``self`` + + EXAMPLE:: + + sage: from sage.tensor.modules.comp import Components + sage: c = Components(ZZ, [1,2,3], 1) + sage: c[:] = 5, 0, -4 + sage: a = c.__neg__() ; a + 1-index components w.r.t. [1, 2, 3] + sage: a[:] + [-5, 0, 4] + sage: a == -c + True + + """ + result = self._new_instance() + for ind, val in self._comp.iteritems(): + result._comp[ind] = - val + return result + + def __add__(self, other): + r""" + Component addition. + + INPUT: + + - ``other`` -- components of the same number of indices and defined + on the same frame as ``self`` + + OUTPUT: + + - components resulting from the addition of ``self`` and ``other`` + + EXAMPLE:: + + sage: from sage.tensor.modules.comp import Components + sage: a = Components(ZZ, [1,2,3], 1) + sage: a[:] = 1, 0, -3 + sage: b = Components(ZZ, [1,2,3], 1) + sage: b[:] = 4, 5, 6 + sage: s = a.__add__(b) ; s + 1-index components w.r.t. [1, 2, 3] + sage: s[:] + [5, 5, 3] + sage: s == a+b + True + + """ + if other == 0: + return +self + if not isinstance(other, Components): + raise TypeError("the second argument for the addition must be " + + "an instance of Components") + if isinstance(other, CompWithSym): + return other + self # to deal properly with symmetries + if other._frame != self._frame: + raise ValueError("the two sets of components are not defined on " + + "the same frame") + if other._nid != self._nid: + raise ValueError("the two sets of components do not have the " + + "same number of indices") + if other._sindex != self._sindex: + raise ValueError("the two sets of components do not have the " + + "same starting index") + result = self.copy() + for ind, val in other._comp.iteritems(): + result[[ind]] += val + return result + + def __radd__(self, other): + r""" + Reflected addition (addition on the right to `other``) + + EXAMPLE:: + + sage: from sage.tensor.modules.comp import Components + sage: a = Components(ZZ, [1,2,3], 1) + sage: a[:] = 1, 0, -3 + sage: b = Components(ZZ, [1,2,3], 1) + sage: b[:] = 4, 5, 6 + sage: s = a.__radd__(b) ; s + 1-index components w.r.t. [1, 2, 3] + sage: s[:] + [5, 5, 3] + sage: s == a+b + True + sage: s = 0 + a ; s + 1-index components w.r.t. [1, 2, 3] + sage: s == a + True + + """ + return self.__add__(other) + + + def __sub__(self, other): + r""" + Component subtraction. + + INPUT: + + - ``other`` -- components, of the same type as ``self`` + + OUTPUT: + + - components resulting from the subtraction of ``other`` from ``self`` + + EXAMPLE:: + + sage: from sage.tensor.modules.comp import Components + sage: a = Components(ZZ, [1,2,3], 1) + sage: a[:] = 1, 0, -3 + sage: b = Components(ZZ, [1,2,3], 1) + sage: b[:] = 4, 5, 6 + sage: s = a.__sub__(b) ; s + 1-index components w.r.t. [1, 2, 3] + sage: s[:] + [-3, -5, -9] + sage: s == a - b + True + + """ + if other == 0: + return +self + return self.__add__(-other) #!# correct, deals properly with + # symmetries, but is probably not optimal + + def __rsub__(self, other): + r""" + Reflected subtraction (subtraction from ``other``). + + EXAMPLES:: + + sage: from sage.tensor.modules.comp import Components + sage: a = Components(ZZ, [1,2,3], 1) + sage: a[:] = 1, 0, -3 + sage: b = Components(ZZ, [1,2,3], 1) + sage: b[:] = 4, 5, 6 + sage: s = a.__rsub__(b) ; s + 1-index components w.r.t. [1, 2, 3] + sage: s[:] + [3, 5, 9] + sage: s == b - a + True + sage: s = 0 - a ; s + 1-index components w.r.t. [1, 2, 3] + sage: s[:] + [-1, 0, 3] + sage: s == -a + True + + """ + return (-self).__add__(other) + + + def __mul__(self, other): + r""" + Component tensor product. + + INPUT: + + - ``other`` -- components, on the same frame as ``self`` + + OUTPUT: + + - the tensor product of ``self`` by ``other`` + + EXAMPLES:: + + sage: from sage.tensor.modules.comp import Components + sage: a = Components(ZZ, [1,2,3], 1) + sage: a[:] = 1, 0, -3 + sage: b = Components(ZZ, [1,2,3], 1) + sage: b[:] = 4, 5, 6 + sage: s = a.__mul__(b) ; s + 2-indices components w.r.t. [1, 2, 3] + sage: s[:] + [ 4 5 6] + [ 0 0 0] + [-12 -15 -18] + sage: s == a*b + True + + """ + if not isinstance(other, Components): + raise TypeError("the second argument for the tensor product " + + "must be an instance of Components") + if other._frame != self._frame: + raise ValueError("the two sets of components are not defined on " + + "the same frame") + if other._sindex != self._sindex: + raise ValueError("the two sets of components do not have the " + + "same starting index") + if isinstance(other, CompWithSym): + sym = [] + if other._sym != []: + for s in other._sym: + ns = tuple(s[i]+self._nid for i in range(len(s))) + sym.append(ns) + antisym = [] + if other._antisym != []: + for s in other._antisym: + ns = tuple(s[i]+self._nid for i in range(len(s))) + antisym.append(ns) + result = CompWithSym(self._ring, self._frame, self._nid + other._nid, + self._sindex, self._output_formatter, sym, + antisym) + elif self._nid == 1 and other._nid == 1: + if self is other: # == would be dangerous here + # The result is symmetric: + result = CompFullySym(self._ring, self._frame, 2, self._sindex, + self._output_formatter) + # The loop below on self._comp.iteritems() and + # other._comp.iteritems() cannot be used in the present case + # (it would not deal correctly with redundant indices) + # So we use a loop specific to the current case and return the + # result: + for ind in result.non_redundant_index_generator(): + result[[ind]] = self[[ind[0]]] * self[[ind[1]]] + return result + else: + result = Components(self._ring, self._frame, 2, self._sindex, + self._output_formatter) + else: + result = Components(self._ring, self._frame, self._nid + other._nid, + self._sindex, self._output_formatter) + for ind_s, val_s in self._comp.iteritems(): + for ind_o, val_o in other._comp.iteritems(): + result._comp[ind_s + ind_o] = val_s * val_o + return result + + + def __rmul__(self, other): + r""" + Reflected multiplication (multiplication on the left by ``other``). + + EXAMPLES:: + + sage: from sage.tensor.modules.comp import Components + sage: a = Components(ZZ, [1,2,3], 1) + sage: a[:] = 1, 0, -3 + sage: s = a.__rmul__(2) ; s + 1-index components w.r.t. [1, 2, 3] + sage: s[:] + [2, 0, -6] + sage: s == 2*a + True + sage: a.__rmul__(0) == 0 + True + + """ + if isinstance(other, Components): + raise NotImplementedError("left tensor product not implemented") + # Left multiplication by a "scalar": + result = self._new_instance() + if other == 0: + return result # because a just created Components is zero + for ind, val in self._comp.iteritems(): + result._comp[ind] = other * val + return result + + + def __div__(self, other): + r""" + Division (by a scalar). + + EXAMPLES:: + + sage: from sage.tensor.modules.comp import Components + sage: a = Components(QQ, [1,2,3], 1) + sage: a[:] = 1, 0, -3 + sage: s = a.__div__(3) ; s + 1-index components w.r.t. [1, 2, 3] + sage: s[:] + [1/3, 0, -1] + sage: s == a/3 + True + sage: 3*s == a + True + + """ + if isinstance(other, Components): + raise NotImplementedError("division by an object of type " + + "Components not implemented") + result = self._new_instance() + for ind, val in self._comp.iteritems(): + result._comp[ind] = val / other + return result + + def trace(self, pos1, pos2): + r""" + Index contraction. + + INPUT: + + - ``pos1`` -- position of the first index for the contraction (with the + convention position=0 for the first slot) + - ``pos2`` -- position of the second index for the contraction + + OUTPUT: + + - set of components resulting from the (pos1, pos2) contraction + + EXAMPLES: + + Self-contraction of a set of components with 2 indices:: + + sage: from sage.tensor.modules.comp import Components + sage: V = VectorSpace(QQ, 3) + sage: c = Components(QQ, V.basis(), 2) + sage: c[:] = [[1,2,3], [4,5,6], [7,8,9]] + sage: c.trace(0,1) + 15 + sage: c[0,0] + c[1,1] + c[2,2] # check + 15 + + Three self-contractions of a set of components with 3 indices:: + + sage: v = Components(QQ, V.basis(), 1) + sage: v[:] = (-1,2,3) + sage: a = c*v ; a + 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: s = a.trace(0,1) ; s # contraction on the first two indices + 1-index components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: s[:] + [-15, 30, 45] + sage: [sum(a[j,j,i] for j in range(3)) for i in range(3)] # check + [-15, 30, 45] + sage: s = a.trace(0,2) ; s[:] # contraction on the first and last indices + [28, 32, 36] + sage: [sum(a[j,i,j] for j in range(3)) for i in range(3)] # check + [28, 32, 36] + sage: s = a.trace(1,2) ; s[:] # contraction on the last two indices + [12, 24, 36] + sage: [sum(a[i,j,j] for j in range(3)) for i in range(3)] # check + [12, 24, 36] + + """ + if self._nid < 2: + raise ValueError("contraction can be perfomed only on " + + "components with at least 2 indices") + if pos1 < 0 or pos1 > self._nid - 1: + raise IndexError("pos1 out of range") + if pos2 < 0 or pos2 > self._nid - 1: + raise IndexError("pos2 out of range") + if pos1 == pos2: + raise IndexError("the two positions must differ for the " + + "contraction to be meaningful") + si = self._sindex + nsi = si + self._dim + if self._nid == 2: + res = 0 + for i in range(si, nsi): + res += self[[i,i]] + return res + else: + # More than 2 indices + result = Components(self._ring, self._frame, self._nid - 2, + self._sindex, self._output_formatter) + if pos1 > pos2: + pos1, pos2 = (pos2, pos1) + for ind, val in self._comp.iteritems(): + if ind[pos1] == ind[pos2]: + # there is a contribution to the contraction + ind_res = ind[:pos1] + ind[pos1+1:pos2] + ind[pos2+1:] + result[[ind_res]] += val + return result + + def contract(self, *args): + r""" + Contraction on one or many indices with another instance of + :class:`Components`. + + INPUT: + + - ``pos1`` -- positions of the indices in ``self`` involved in the + contraction; ``pos1`` must be a sequence of integers, with 0 standing + for the first index position, 1 for the second one, etc. If ``pos1`` + is not provided, a single contraction on the last index position of + ``self`` is assumed + - ``other`` -- the set of components to contract with + - ``pos2`` -- positions of the indices in ``other`` involved in the + contraction, with the same conventions as for ``pos1``. If ``pos2`` + is not provided, a single contraction on the first index position of + ``other`` is assumed + + OUTPUT: + + - set of components resulting from the contraction + + EXAMPLES: + + Contraction of a 1-index set of components with a 2-index one:: + + sage: from sage.tensor.modules.comp import Components + sage: V = VectorSpace(QQ, 3) + sage: a = Components(QQ, V.basis(), 1) + sage: a[:] = (-1, 2, 3) + sage: b = Components(QQ, V.basis(), 2) + sage: b[:] = [[1,2,3], [4,5,6], [7,8,9]] + sage: s = a.contract(0, b, 0) ; s + 1-index components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: s[:] + [28, 32, 36] + sage: [sum(a[j]*b[j,i] for j in range(3)) for i in range(3)] # check + [28, 32, 36] + sage: s = a.contract(0, b, 1) ; s[:] + [12, 24, 36] + sage: [sum(a[j]*b[i,j] for j in range(3)) for i in range(3)] # check + [12, 24, 36] + + Contraction on 2 indices:: + + sage: c = a*b ; c + 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: s = c.contract(1,2, b, 0,1) ; s + 1-index components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: s[:] + [-285, 570, 855] + sage: [sum(sum(c[i,j,k]*b[j,k] for k in range(3)) # check + ....: for j in range(3)) for i in range(3)] + [-285, 570, 855] + + Consistency check with :meth:`trace`:: + + sage: b = a*a ; b # the tensor product of a with itself + Fully symmetric 2-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: b[:] + [ 1 -2 -3] + [-2 4 6] + [-3 6 9] + sage: b.trace(0,1) + 14 + sage: a.contract(0, a, 0) == b.trace(0,1) + True + + """ + # + # Treatment of the input + # + nargs = len(args) + for i, arg in enumerate(args): + if isinstance(arg, Components): + other = arg + it = i + break + else: + raise ValueError("a set of components must be provided in the " + + "argument list") + if it == 0: + pos1 = (self._nid - 1,) + else: + pos1 = args[:it] + if it == nargs-1: + pos2 = (0,) + else: + pos2 = args[it+1:] + ncontr = len(pos1) # number of contractions + if len(pos2) != ncontr: + raise TypeError("Different number of indices for the contraction.") + if other._frame != self._frame: + raise TypeError("The two sets of components are not defined on " + + "the same frame.") + if other._sindex != self._sindex: + raise TypeError("The two sets of components do not have the " + + "same starting index.") + contractions = [(pos1[i], pos2[i]) for i in range(ncontr)] + res_nid = self._nid + other._nid - 2*ncontr + # + # Special case of a scalar result + # + if res_nid == 0: + # To generate the indices tuples (of size ncontr) involved in the + # the contraction, we create an empty instance of Components with + # ncontr indices and call the method index_generator() on it: + comp_for_contr = Components(self._ring, self._frame, ncontr, + start_index=self._sindex) + res = 0 + for ind in comp_for_contr.index_generator(): + res += self[[ind]] * other[[ind]] + return res + # + # Positions of self and other indices in the result + # (None = the position is involved in a contraction and therefore + # does not appear in the final result) + # + pos_s = [None for i in range(self._nid)] # initialization + pos_o = [None for i in range(other._nid)] # initialization + shift = 0 + for pos in range(self._nid): + for contract_pair in contractions: + if pos == contract_pair[0]: + shift += 1 + break + else: + pos_s[pos] = pos - shift + for pos in range(other._nid): + for contract_pair in contractions: + if pos == contract_pair[1]: + shift += 1 + break + else: + pos_o[pos] = self._nid + pos - shift + rev_s = [pos_s.index(i) for i in range(self._nid-ncontr)] + rev_o = [pos_o.index(i) for i in range(self._nid-ncontr, res_nid)] + # + # Determination of the symmetries of the result + # + max_len_sym = 0 # maximum length of symmetries in the result + max_len_antisym = 0 # maximum length of antisymmetries in the result + if res_nid > 1: # no need to search for symmetries if res_nid == 1 + if isinstance(self, CompWithSym): + s_sym = self._sym + s_antisym = self._antisym + else: + s_sym = [] + s_antisym = [] + if isinstance(other, CompWithSym): + o_sym = other._sym + o_antisym = other._antisym + else: + o_sym = [] + o_antisym = [] + # print "s_sym, s_antisym: ", s_sym, s_antisym + # print "o_sym, o_antisym: ", o_sym, o_antisym + res_sym = [] + res_antisym = [] + for isym in s_sym: + r_isym = [] + for pos in isym: + if pos_s[pos] is not None: + r_isym.append(pos_s[pos]) + if len(r_isym) > 1: + res_sym.append(r_isym) + max_len_sym = max(max_len_sym, len(r_isym)) + for isym in s_antisym: + r_isym = [] + for pos in isym: + if pos_s[pos] is not None: + r_isym.append(pos_s[pos]) + if len(r_isym) > 1: + res_antisym.append(r_isym) + max_len_antisym = max(max_len_antisym, len(r_isym)) + for isym in o_sym: + r_isym = [] + for pos in isym: + if pos_o[pos] is not None: + r_isym.append(pos_o[pos]) + if len(r_isym) > 1: + res_sym.append(r_isym) + max_len_sym = max(max_len_sym, len(r_isym)) + for isym in o_antisym: + r_isym = [] + for pos in isym: + if pos_o[pos] is not None: + r_isym.append(pos_o[pos]) + if len(r_isym) > 1: + res_antisym.append(r_isym) + max_len_antisym = max(max_len_antisym, len(r_isym)) + # print "res_sym: ", res_sym + # print "res_antisym: ", res_antisym + # print "max_len_sym: ", max_len_sym + # print "max_len_antisym: ", max_len_antisym + # + # Construction of the result object in view of the remaining symmetries: + # + if max_len_sym == 0 and max_len_antisym == 0: + res = Components(self._ring, self._frame, res_nid, + start_index=self._sindex, + output_formatter=self._output_formatter) + elif max_len_sym == res_nid: + res = CompFullySym(self._ring, self._frame, res_nid, + start_index=self._sindex, + output_formatter=self._output_formatter) + elif max_len_antisym == res_nid: + res = CompFullyAntiSym(self._ring, self._frame, res_nid, + start_index=self._sindex, + output_formatter=self._output_formatter) + else: + res = CompWithSym(self._ring, self._frame, res_nid, + start_index=self._sindex, + output_formatter=self._output_formatter, + sym=res_sym, antisym=res_antisym) + # + # Performing the contraction + # + # To generate the indices tuples (of size ncontr) involved in the + # the contraction, we create an empty instance of Components with + # ncontr indices and call the method index_generator() on it: + comp_for_contr = Components(self._ring, self._frame, ncontr, + start_index=self._sindex) + shift_o = self._nid - ncontr + for ind in res.non_redundant_index_generator(): + ind_s = [None for i in range(self._nid)] # initialization + ind_o = [None for i in range(other._nid)] # initialization + for i, pos in enumerate(rev_s): + ind_s[pos] = ind[i] + for i, pos in enumerate(rev_o): + ind_o[pos] = ind[shift_o+i] + sm = 0 + for ind_c in comp_for_contr.index_generator(): + ic = 0 + for pos_s, pos_o in contractions: + k = ind_c[ic] + ind_s[pos_s] = k + ind_o[pos_o] = k + ic += 1 + sm += self[[ind_s]] * other[[ind_o]] + res[[ind]] = sm + return res + + + def index_generator(self): + r""" + Generator of indices. + + OUTPUT: + + - an iterable index + + EXAMPLES: + + Indices on a 3-dimensional vector space:: + + sage: from sage.tensor.modules.comp import Components + sage: V = VectorSpace(QQ,3) + sage: c = Components(QQ, V.basis(), 1) + sage: for ind in c.index_generator(): print ind, + (0,) (1,) (2,) + sage: c = Components(QQ, V.basis(), 1, start_index=1) + sage: for ind in c.index_generator(): print ind, + (1,) (2,) (3,) + sage: c = Components(QQ, V.basis(), 2) + sage: for ind in c.index_generator(): print ind, + (0, 0) (0, 1) (0, 2) (1, 0) (1, 1) (1, 2) (2, 0) (2, 1) (2, 2) + + """ + si = self._sindex + imax = self._dim - 1 + si + ind = [si for k in range(self._nid)] + ind_end = [si for k in range(self._nid)] + ind_end[0] = imax+1 + while ind != ind_end: + yield tuple(ind) + ret = 1 + for pos in range(self._nid-1,-1,-1): + if ind[pos] != imax: + ind[pos] += ret + ret = 0 + elif ret == 1: + if pos == 0: + ind[pos] = imax + 1 # end point reached + else: + ind[pos] = si + ret = 1 + + def non_redundant_index_generator(self): + r""" + Generator of non redundant indices. + + In the absence of declared symmetries, all possible indices are + generated. So this method is equivalent to :meth:`index_generator`. + Only versions for derived classes with symmetries or antisymmetries + are not trivial. + + OUTPUT: + + - an iterable index + + EXAMPLES: + + Indices on a 3-dimensional vector space:: + + sage: from sage.tensor.modules.comp import Components + sage: V = VectorSpace(QQ,3) + sage: c = Components(QQ, V.basis(), 2) + sage: for ind in c.non_redundant_index_generator(): print ind, + (0, 0) (0, 1) (0, 2) (1, 0) (1, 1) (1, 2) (2, 0) (2, 1) (2, 2) + sage: c = Components(QQ, V.basis(), 2, start_index=1) + sage: for ind in c.non_redundant_index_generator(): print ind, + (1, 1) (1, 2) (1, 3) (2, 1) (2, 2) (2, 3) (3, 1) (3, 2) (3, 3) + + """ + for ind in self.index_generator(): + yield ind + + + def symmetrize(self, *pos): + r""" + Symmetrization over the given index positions. + + INPUT: + + - ``pos`` -- list of index positions involved in the + symmetrization (with the convention position=0 for the first slot); + if none, the symmetrization is performed over all the indices + + OUTPUT: + + - an instance of :class:`CompWithSym` describing the symmetrized + components + + EXAMPLES: + + Symmetrization of 2-indices components:: + + sage: from sage.tensor.modules.comp import Components + sage: V = VectorSpace(QQ, 3) + sage: c = Components(QQ, V.basis(), 2) + sage: c[:] = [[1,2,3], [4,5,6], [7,8,9]] + sage: s = c.symmetrize() ; s + Fully symmetric 2-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: c[:], s[:] + ( + [1 2 3] [1 3 5] + [4 5 6] [3 5 7] + [7 8 9], [5 7 9] + ) + sage: c.symmetrize() == c.symmetrize(0,1) + True + + Full symmetrization of 3-indices components:: + + sage: c = Components(QQ, V.basis(), 3) + sage: c[:] = [[[1,2,3], [4,5,6], [7,8,9]], [[10,11,12], [13,14,15], [16,17,18]], [[19,20,21], [22,23,24], [25,26,27]]] + sage: s = c.symmetrize() ; s + Fully symmetric 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: c[:], s[:] + ([[[1, 2, 3], [4, 5, 6], [7, 8, 9]], + [[10, 11, 12], [13, 14, 15], [16, 17, 18]], + [[19, 20, 21], [22, 23, 24], [25, 26, 27]]], + [[[1, 16/3, 29/3], [16/3, 29/3, 14], [29/3, 14, 55/3]], + [[16/3, 29/3, 14], [29/3, 14, 55/3], [14, 55/3, 68/3]], + [[29/3, 14, 55/3], [14, 55/3, 68/3], [55/3, 68/3, 27]]]) + sage: all(s[i,j,k] == (c[i,j,k]+c[i,k,j]+c[j,k,i]+c[j,i,k]+c[k,i,j]+c[k,j,i])/6 # Check of the result: + ....: for i in range(3) for j in range(3) for k in range(3)) + True + sage: c.symmetrize() == c.symmetrize(0,1,2) + True + + Partial symmetrization of 3-indices components:: + + sage: s = c.symmetrize(0,1) ; s # symmetrization on the first two indices + 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with symmetry on the index positions (0, 1) + sage: c[:], s[:] + ([[[1, 2, 3], [4, 5, 6], [7, 8, 9]], + [[10, 11, 12], [13, 14, 15], [16, 17, 18]], + [[19, 20, 21], [22, 23, 24], [25, 26, 27]]], + [[[1, 2, 3], [7, 8, 9], [13, 14, 15]], + [[7, 8, 9], [13, 14, 15], [19, 20, 21]], + [[13, 14, 15], [19, 20, 21], [25, 26, 27]]]) + sage: all(s[i,j,k] == (c[i,j,k]+c[j,i,k])/2 # Check of the result: + ....: for i in range(3) for j in range(3) for k in range(3)) + True + sage: s = c.symmetrize(1,2) ; s # symmetrization on the last two indices + 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with symmetry on the index positions (1, 2) + sage: c[:], s[:] + ([[[1, 2, 3], [4, 5, 6], [7, 8, 9]], + [[10, 11, 12], [13, 14, 15], [16, 17, 18]], + [[19, 20, 21], [22, 23, 24], [25, 26, 27]]], + [[[1, 3, 5], [3, 5, 7], [5, 7, 9]], + [[10, 12, 14], [12, 14, 16], [14, 16, 18]], + [[19, 21, 23], [21, 23, 25], [23, 25, 27]]]) + sage: all(s[i,j,k] == (c[i,j,k]+c[i,k,j])/2 # Check of the result: + ....: for i in range(3) for j in range(3) for k in range(3)) + True + sage: s = c.symmetrize(0,2) ; s # symmetrization on the first and last indices + 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with symmetry on the index positions (0, 2) + sage: c[:], s[:] + ([[[1, 2, 3], [4, 5, 6], [7, 8, 9]], + [[10, 11, 12], [13, 14, 15], [16, 17, 18]], + [[19, 20, 21], [22, 23, 24], [25, 26, 27]]], + [[[1, 6, 11], [4, 9, 14], [7, 12, 17]], + [[6, 11, 16], [9, 14, 19], [12, 17, 22]], + [[11, 16, 21], [14, 19, 24], [17, 22, 27]]]) + sage: all(s[i,j,k] == (c[i,j,k]+c[k,j,i])/2 # Check of the result: + ....: for i in range(3) for j in range(3) for k in range(3)) + True + + """ + from sage.groups.perm_gps.permgroup_named import SymmetricGroup + if not pos: + pos = range(self._nid) + else: + if len(pos) < 2: + raise ValueError("at least two index positions must be given") + if len(pos) > self._nid: + raise ValueError("number of index positions larger than the " + "total number of indices") + n_sym = len(pos) # number of indices involved in the symmetry + if n_sym == self._nid: + result = CompFullySym(self._ring, self._frame, self._nid, self._sindex, + self._output_formatter) + else: + result = CompWithSym(self._ring, self._frame, self._nid, self._sindex, + self._output_formatter, sym=pos) + sym_group = SymmetricGroup(n_sym) + for ind in result.non_redundant_index_generator(): + sum = 0 + for perm in sym_group.list(): + # action of the permutation on [0,1,...,n_sym-1]: + perm_action = map(lambda x: x-1, perm.domain()) + ind_perm = list(ind) + for k in range(n_sym): + ind_perm[pos[perm_action[k]]] = ind[pos[k]] + sum += self[[ind_perm]] + result[[ind]] = sum / sym_group.order() + return result + + def antisymmetrize(self, *pos): + r""" + Antisymmetrization over the given index positions + + INPUT: + + - ``pos`` -- list of index positions involved in the antisymmetrization + (with the convention position=0 for the first slot); if none, the + antisymmetrization is performed over all the indices + + OUTPUT: + + - an instance of :class:`CompWithSym` describing the antisymmetrized + components. + + EXAMPLES: + + Antisymmetrization of 2-indices components:: + + sage: from sage.tensor.modules.comp import Components + sage: V = VectorSpace(QQ, 3) + sage: c = Components(QQ, V.basis(), 2) + sage: c[:] = [[1,2,3], [4,5,6], [7,8,9]] + sage: s = c.antisymmetrize() ; s + Fully antisymmetric 2-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: c[:], s[:] + ( + [1 2 3] [ 0 -1 -2] + [4 5 6] [ 1 0 -1] + [7 8 9], [ 2 1 0] + ) + sage: c.antisymmetrize() == c.antisymmetrize(0,1) + True + + Full antisymmetrization of 3-indices components:: + + sage: c = Components(QQ, V.basis(), 3) + sage: c[:] = [[[-1,-2,3], [4,-5,4], [-7,8,9]], [[10,10,12], [13,-14,15], [-16,17,19]], [[-19,20,21], [1,2,3], [-25,26,27]]] + sage: s = c.antisymmetrize() ; s + Fully antisymmetric 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: c[:], s[:] + ([[[-1, -2, 3], [4, -5, 4], [-7, 8, 9]], + [[10, 10, 12], [13, -14, 15], [-16, 17, 19]], + [[-19, 20, 21], [1, 2, 3], [-25, 26, 27]]], + [[[0, 0, 0], [0, 0, -13/6], [0, 13/6, 0]], + [[0, 0, 13/6], [0, 0, 0], [-13/6, 0, 0]], + [[0, -13/6, 0], [13/6, 0, 0], [0, 0, 0]]]) + sage: all(s[i,j,k] == (c[i,j,k]-c[i,k,j]+c[j,k,i]-c[j,i,k]+c[k,i,j]-c[k,j,i])/6 # Check of the result: + ....: for i in range(3) for j in range(3) for k in range(3)) + True + sage: c.symmetrize() == c.symmetrize(0,1,2) + True + + Partial antisymmetrization of 3-indices components:: + + sage: s = c.antisymmetrize(0,1) ; s # antisymmetrization on the first two indices + 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with antisymmetry on the index positions (0, 1) + sage: c[:], s[:] + ([[[-1, -2, 3], [4, -5, 4], [-7, 8, 9]], + [[10, 10, 12], [13, -14, 15], [-16, 17, 19]], + [[-19, 20, 21], [1, 2, 3], [-25, 26, 27]]], + [[[0, 0, 0], [-3, -15/2, -4], [6, -6, -6]], + [[3, 15/2, 4], [0, 0, 0], [-17/2, 15/2, 8]], + [[-6, 6, 6], [17/2, -15/2, -8], [0, 0, 0]]]) + sage: all(s[i,j,k] == (c[i,j,k]-c[j,i,k])/2 # Check of the result: + ....: for i in range(3) for j in range(3) for k in range(3)) + True + sage: s = c.antisymmetrize(1,2) ; s # antisymmetrization on the last two indices + 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with antisymmetry on the index positions (1, 2) + sage: c[:], s[:] + ([[[-1, -2, 3], [4, -5, 4], [-7, 8, 9]], + [[10, 10, 12], [13, -14, 15], [-16, 17, 19]], + [[-19, 20, 21], [1, 2, 3], [-25, 26, 27]]], + [[[0, -3, 5], [3, 0, -2], [-5, 2, 0]], + [[0, -3/2, 14], [3/2, 0, -1], [-14, 1, 0]], + [[0, 19/2, 23], [-19/2, 0, -23/2], [-23, 23/2, 0]]]) + sage: all(s[i,j,k] == (c[i,j,k]-c[i,k,j])/2 # Check of the result: + ....: for i in range(3) for j in range(3) for k in range(3)) + True + sage: s = c.antisymmetrize(0,2) ; s # antisymmetrization on the first and last indices + 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with antisymmetry on the index positions (0, 2) + sage: c[:], s[:] + ([[[-1, -2, 3], [4, -5, 4], [-7, 8, 9]], + [[10, 10, 12], [13, -14, 15], [-16, 17, 19]], + [[-19, 20, 21], [1, 2, 3], [-25, 26, 27]]], + [[[0, -6, 11], [0, -9, 3/2], [0, 12, 17]], + [[6, 0, -4], [9, 0, 13/2], [-12, 0, -7/2]], + [[-11, 4, 0], [-3/2, -13/2, 0], [-17, 7/2, 0]]]) + sage: all(s[i,j,k] == (c[i,j,k]-c[k,j,i])/2 # Check of the result: + ....: for i in range(3) for j in range(3) for k in range(3)) + True + + The order of index positions in the argument does not matter:: + + sage: c.antisymmetrize(1,0) == c.antisymmetrize(0,1) + True + sage: c.antisymmetrize(2,1) == c.antisymmetrize(1,2) + True + sage: c.antisymmetrize(2,0) == c.antisymmetrize(0,2) + True + + """ + from sage.groups.perm_gps.permgroup_named import SymmetricGroup + if not pos: + pos = range(self._nid) + else: + if len(pos) < 2: + raise ValueError("at least two index positions must be given") + if len(pos) > self._nid: + raise ValueError("number of index positions larger than the " + "total number of indices") + n_sym = len(pos) # number of indices involved in the antisymmetry + if n_sym == self._nid: + result = CompFullyAntiSym(self._ring, self._frame, self._nid, + self._sindex, self._output_formatter) + else: + result = CompWithSym(self._ring, self._frame, self._nid, self._sindex, + self._output_formatter, antisym=pos) + sym_group = SymmetricGroup(n_sym) + for ind in result.non_redundant_index_generator(): + sum = 0 + for perm in sym_group.list(): + # action of the permutation on [0,1,...,n_sym-1]: + perm_action = map(lambda x: x-1, perm.domain()) + ind_perm = list(ind) + for k in range(n_sym): + ind_perm[pos[perm_action[k]]] = ind[pos[k]] + if perm.sign() == 1: + sum += self[[ind_perm]] + else: + sum -= self[[ind_perm]] + result[[ind]] = sum / sym_group.order() + return result + + def _matrix_(self): + r""" + Convert a set of ring components with 2 indices into a matrix. + + EXAMPLE:: + + sage: from sage.tensor.modules.comp import Components + sage: V = VectorSpace(QQ, 3) + sage: c = Components(QQ, V.basis(), 2, start_index=1) + sage: c[:] = [[-1,2,3], [4,-5,6], [7,8,-9]] + sage: c._matrix_() + [-1 2 3] + [ 4 -5 6] + [ 7 8 -9] + + sage: matrix(c) == c._matrix_() + True + + """ + from sage.matrix.constructor import matrix + if self._nid != 2: + raise ValueError("the set of components must have 2 indices") + si = self._sindex + nsi = self._dim + si + tab = [[self[[i,j]] for j in range(si, nsi)] for i in range(si, nsi)] + return matrix(tab) + + +#****************************************************************************** + +class CompWithSym(Components): + r""" + Indexed set of ring elements forming some components with respect to a + given "frame", with symmetries or antisymmetries regarding permutations + of the indices. + + The "frame" can be a basis of some vector space or a vector frame on some + manifold (i.e. a field of bases). + The stored quantities can be tensor components or non-tensorial quantities, + such as connection coefficients or structure coefficents. + + Subclasses of :class:`CompWithSym` are + + * :class:`CompFullySym` for fully symmetric components. + * :class:`CompFullyAntiSym` for fully antisymmetric components. + + INPUT: + + - ``ring`` -- commutative ring in which each component takes its value + - ``frame`` -- frame with respect to which the components are defined; + whatever type ``frame`` is, it should have some method ``__len__()`` + implemented, so that ``len(frame)`` returns the dimension, i.e. the size + of a single index range + - ``nb_indices`` -- number of indices labeling the components + - ``start_index`` -- (default: 0) first value of a single index; + accordingly a component index i must obey + ``start_index <= i <= start_index + dim - 1``, where ``dim = len(frame)``. + - ``output_formatter`` -- (default: ``None``) function or unbound + method called to format the output of the component access + operator ``[...]`` (method __getitem__); ``output_formatter`` must take + 1 or 2 arguments: the 1st argument must be an instance of ``ring`` and + the second one, if any, some format specification. + - ``sym`` -- (default: ``None``) a symmetry or a list of symmetries among + the indices: each symmetry is described by a tuple containing the + positions of the involved indices, with the convention ``position=0`` + for the first slot; for instance: + + * ``sym = (0, 1)`` for a symmetry between the 1st and 2nd indices + * ``sym = [(0,2), (1,3,4)]`` for a symmetry between the 1st and 3rd + indices and a symmetry between the 2nd, 4th and 5th indices. + + - ``antisym`` -- (default: ``None``) antisymmetry or list of antisymmetries + among the indices, with the same convention as for ``sym`` + + EXAMPLES: + + Symmetric components with 2 indices:: + + sage: from sage.tensor.modules.comp import Components, CompWithSym + sage: V = VectorSpace(QQ,3) + sage: c = CompWithSym(QQ, V.basis(), 2, sym=(0,1)) # for demonstration only: it is preferable to use CompFullySym in this case + sage: c[0,1] = 3 + sage: c[:] # note that c[1,0] has been set automatically + [0 3 0] + [3 0 0] + [0 0 0] + + Antisymmetric components with 2 indices:: + + sage: c = CompWithSym(QQ, V.basis(), 2, antisym=(0,1)) # for demonstration only: it is preferable to use CompFullyAntiSym in this case + sage: c[0,1] = 3 + sage: c[:] # note that c[1,0] has been set automatically + [ 0 3 0] + [-3 0 0] + [ 0 0 0] + + Internally, only non-redundant components are stored:: + + sage: c._comp + {(0, 1): 3} + + Components with 6 indices, symmetric among 3 indices (at position + `(0, 1, 5)`) and antisymmetric among 2 indices (at position `(2, 4)`):: + + sage: c = CompWithSym(QQ, V.basis(), 6, sym=(0,1,5), antisym=(2,4)) + sage: c[0,1,2,0,1,2] = 3 + sage: c[1,0,2,0,1,2] # symmetry between indices in position 0 and 1 + 3 + sage: c[2,1,2,0,1,0] # symmetry between indices in position 0 and 5 + 3 + sage: c[0,2,2,0,1,1] # symmetry between indices in position 1 and 5 + 3 + sage: c[0,1,1,0,2,2] # antisymmetry between indices in position 2 and 4 + -3 + + Components with 4 indices, antisymmetric with respect to the first pair of + indices as well as with the second pair of indices:: + + sage: c = CompWithSym(QQ, V.basis(), 4, antisym=[(0,1),(2,3)]) + sage: c[0,1,0,1] = 3 + sage: c[1,0,0,1] # antisymmetry on the first pair of indices + -3 + sage: c[0,1,1,0] # antisymmetry on the second pair of indices + -3 + sage: c[1,0,1,0] # consequence of the above + 3 + + .. RUBRIC:: ARITHMETIC EXAMPLES + + Addition of a symmetric set of components with a non-symmetric one: the + symmetry is lost:: + + sage: V = VectorSpace(QQ, 3) + sage: a = Components(QQ, V.basis(), 2) + sage: a[:] = [[1,-2,3], [4,5,-6], [-7,8,9]] + sage: b = CompWithSym(QQ, V.basis(), 2, sym=(0,1)) # for demonstration only: it is preferable to declare b = CompFullySym(QQ, V.basis(), 2) + sage: b[0,0], b[0,1], b[0,2] = 1, 2, 3 + sage: b[1,1], b[1,2] = 5, 7 + sage: b[2,2] = 11 + sage: s = a + b ; s + 2-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: a[:], b[:], s[:] + ( + [ 1 -2 3] [ 1 2 3] [ 2 0 6] + [ 4 5 -6] [ 2 5 7] [ 6 10 1] + [-7 8 9], [ 3 7 11], [-4 15 20] + ) + sage: a + b == b + a + True + + Addition of two symmetric set of components: the symmetry is preserved:: + + sage: c = CompWithSym(QQ, V.basis(), 2, sym=(0,1)) # for demonstration only: it is preferable to declare c = CompFullySym(QQ, V.basis(), 2) + sage: c[0,0], c[0,1], c[0,2] = -4, 7, -8 + sage: c[1,1], c[1,2] = 2, -4 + sage: c[2,2] = 2 + sage: s = b + c ; s + 2-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with symmetry on the index positions (0, 1) + sage: b[:], c[:], s[:] + ( + [ 1 2 3] [-4 7 -8] [-3 9 -5] + [ 2 5 7] [ 7 2 -4] [ 9 7 3] + [ 3 7 11], [-8 -4 2], [-5 3 13] + ) + sage: b + c == c + b + True + + Check of the addition with counterparts not declared symmetric:: + + sage: bn = Components(QQ, V.basis(), 2) + sage: bn[:] = b[:] + sage: bn == b + True + sage: cn = Components(QQ, V.basis(), 2) + sage: cn[:] = c[:] + sage: cn == c + True + sage: bn + cn == b + c + True + + Addition of an antisymmetric set of components with a non-symmetric one: + the antisymmetry is lost:: + + sage: d = CompWithSym(QQ, V.basis(), 2, antisym=(0,1)) # for demonstration only: it is preferable to declare d = CompFullyAntiSym(QQ, V.basis(), 2) + sage: d[0,1], d[0,2], d[1,2] = 4, -1, 3 + sage: s = a + d ; s + 2-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: a[:], d[:], s[:] + ( + [ 1 -2 3] [ 0 4 -1] [ 1 2 2] + [ 4 5 -6] [-4 0 3] [ 0 5 -3] + [-7 8 9], [ 1 -3 0], [-6 5 9] + ) + sage: d + a == a + d + True + + Addition of two antisymmetric set of components: the antisymmetry is preserved:: + + sage: e = CompWithSym(QQ, V.basis(), 2, antisym=(0,1)) # for demonstration only: it is preferable to declare e = CompFullyAntiSym(QQ, V.basis(), 2) + sage: e[0,1], e[0,2], e[1,2] = 2, 3, -1 + sage: s = d + e ; s + 2-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with antisymmetry on the index positions (0, 1) + sage: d[:], e[:], s[:] + ( + [ 0 4 -1] [ 0 2 3] [ 0 6 2] + [-4 0 3] [-2 0 -1] [-6 0 2] + [ 1 -3 0], [-3 1 0], [-2 -2 0] + ) + sage: e + d == d + e + True + + """ + def __init__(self, ring, frame, nb_indices, start_index=0, + output_formatter=None, sym=None, antisym=None): + r""" + TEST:: + + sage: from sage.tensor.modules.comp import CompWithSym + sage: C = CompWithSym(ZZ, [1,2,3], 4, sym=(0,1), antisym=(2,3)) + sage: TestSuite(C).run() + + """ + Components.__init__(self, ring, frame, nb_indices, start_index, + output_formatter) + self._sym = [] + if sym is not None and sym != []: + if isinstance(sym[0], (int, Integer)): + # a single symmetry is provided as a tuple -> 1-item list: + sym = [tuple(sym)] + for isym in sym: + if len(isym) < 2: + raise IndexError("at least two index positions must be " + + "provided to define a symmetry") + for i in isym: + if i<0 or i>self._nid-1: + raise IndexError("invalid index position: " + str(i) + + " not in [0," + str(self._nid-1) + "]") + self._sym.append(tuple(isym)) + self._antisym = [] + if antisym is not None and antisym != []: + if isinstance(antisym[0], (int, Integer)): + # a single antisymmetry is provided as a tuple -> 1-item list: + antisym = [tuple(antisym)] + for isym in antisym: + if len(isym) < 2: + raise IndexError("at least two index positions must be " + + "provided to define an antisymmetry") + for i in isym: + if i<0 or i>self._nid-1: + raise IndexError("invalid index position: " + str(i) + + " not in [0," + str(self._nid-1) + "]") + self._antisym.append(tuple(isym)) + # Final consistency check: + index_list = [] + for isym in self._sym: + index_list += isym + for isym in self._antisym: + index_list += isym + if len(index_list) != len(set(index_list)): + # There is a repeated index position: + raise IndexError("incompatible lists of symmetries: the same " + + "index position appears more then once") + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLES:: + + sage: from sage.tensor.modules.comp import CompWithSym + sage: CompWithSym(ZZ, [1,2,3], 4, sym=(0,1)) + 4-indices components w.r.t. [1, 2, 3], + with symmetry on the index positions (0, 1) + sage: CompWithSym(ZZ, [1,2,3], 4, sym=(0,1), antisym=(2,3)) + 4-indices components w.r.t. [1, 2, 3], + with symmetry on the index positions (0, 1), + with antisymmetry on the index positions (2, 3) + + """ + description = str(self._nid) + if self._nid == 1: + description += "-index" + else: + description += "-indices" + description += " components w.r.t. " + str(self._frame) + for isym in self._sym: + description += ", with symmetry on the index positions " + \ + str(tuple(isym)) + for isym in self._antisym: + description += ", with antisymmetry on the index positions " + \ + str(tuple(isym)) + return description + + def _new_instance(self): + r""" + Create a :class:`CompWithSym` instance w.r.t. the same frame, + and with the same number of indices and the same symmetries. + + EXAMPLES:: + + sage: from sage.tensor.modules.comp import CompWithSym + sage: c = CompWithSym(ZZ, [1,2,3], 4, sym=(0,1)) + sage: a = c._new_instance() ; a + 4-indices components w.r.t. [1, 2, 3], with symmetry on the index positions (0, 1) + + """ + return CompWithSym(self._ring, self._frame, self._nid, self._sindex, + self._output_formatter, self._sym, self._antisym) + + def _ordered_indices(self, indices): + r""" + Given a set of indices, return a set of indices with the indices + at the positions of symmetries or antisymmetries being ordered, + as well as some antisymmetry indicator. + + INPUT: + + - ``indices`` -- list of indices (possibly a single integer if + self is a 1-index object) + + OUTPUT: + + - a pair ``(s,ind)`` where ``ind`` is a tuple that differs from the + original list of indices by a reordering at the positions of + symmetries and antisymmetries and + + * ``s = 0`` if the value corresponding to ``indices`` vanishes by + antisymmetry (repeated indices); `ind` is then set to ``None`` + * ``s = 1`` if the value corresponding to ``indices`` is the same as + that corresponding to ``ind`` + * ``s = -1`` if the value corresponding to ``indices`` is the + opposite of that corresponding to ``ind`` + + EXAMPLES:: + + sage: from sage.tensor.modules.comp import CompWithSym + sage: c = CompWithSym(ZZ, [1,2,3], 4, sym=(0,1), antisym=(2,3)) + sage: c._ordered_indices([0,1,1,2]) + (1, (0, 1, 1, 2)) + sage: c._ordered_indices([1,0,1,2]) + (1, (0, 1, 1, 2)) + sage: c._ordered_indices([0,1,2,1]) + (-1, (0, 1, 1, 2)) + sage: c._ordered_indices([0,1,2,2]) + (0, None) + + """ + from sage.combinat.permutation import Permutation + ind = list(self._check_indices(indices)) + for isym in self._sym: + indsym = [] + for pos in isym: + indsym.append(ind[pos]) + indsym_ordered = sorted(indsym) + for k, pos in enumerate(isym): + ind[pos] = indsym_ordered[k] + sign = 1 + for isym in self._antisym: + indsym = [] + for pos in isym: + indsym.append(ind[pos]) + # Returns zero if some index appears twice: + if len(indsym) != len(set(indsym)): + return (0, None) + # From here, all the indices in indsym are distinct and we need + # to determine whether they form an even permutation of their + # ordered series + indsym_ordered = sorted(indsym) + for k, pos in enumerate(isym): + ind[pos] = indsym_ordered[k] + if indsym_ordered != indsym: + # Permutation linking indsym_ordered to indsym: + # (the +1 is required to fulfill the convention of Permutation) + perm = [indsym.index(i) +1 for i in indsym_ordered] + #c# print "indsym_ordered, indsym: ", indsym_ordered, indsym + #c# print "Permutation: ", Permutation(perm), " signature = ", \ + #c# Permutation(perm).signature() + sign *= Permutation(perm).signature() + ind = tuple(ind) + return (sign, ind) + + def __getitem__(self, args): + r""" + Return the component corresponding to the given indices. + + INPUT: + + - ``args`` -- list of indices (possibly a single integer if + self is a 1-index object) or the character ``:`` for the full list + of components + + OUTPUT: + + - the component corresponding to ``args`` or, if ``args`` = ``:``, + the full list of components, in the form ``T[i][j]...`` for the components + `T_{ij...}` (for a 2-indices object, a matrix is returned). + + EXAMPLES:: + + sage: from sage.tensor.modules.comp import CompWithSym + sage: c = CompWithSym(ZZ, [1,2,3], 4, sym=(0,1), antisym=(2,3)) + sage: c.__getitem__((0,1,1,2)) # uninitialized components are zero + 0 + sage: c[0,1,1,2] = 5 + sage: c.__getitem__((0,1,1,2)) + 5 + sage: c.__getitem__((1,0,1,2)) + 5 + sage: c.__getitem__((0,1,2,1)) + -5 + sage: c[0,1,2,1] + -5 + + """ + no_format = self._output_formatter is None + format_type = None # default value, possibly redefined below + if isinstance(args, list): # case of [[...]] syntax + no_format = True + if isinstance(args[0], slice): + indices = args[0] + elif isinstance(args[0], (tuple, list)): # to ensure equivalence between + indices = args[0] # [[(i,j,...)]] or [[[i,j,...]]] and [[i,j,...]] + else: + indices = tuple(args) + else: + # Determining from the input the list of indices and the format + if isinstance(args, (int, Integer, slice)): + indices = args + elif isinstance(args[0], slice): + indices = args[0] + if len(args) == 2: + format_type = args[1] + elif len(args) == self._nid: + indices = args + else: + format_type = args[-1] + indices = args[:-1] + if isinstance(indices, slice): + return self._get_list(indices, no_format, format_type) + else: + sign, ind = self._ordered_indices(indices) + if (sign == 0) or (ind not in self._comp): # the value is zero: + if no_format: + return self._ring.zero() + elif format_type is None: + return self._output_formatter(self._ring.zero()) + else: + return self._output_formatter(self._ring.zero(), + format_type) + else: # non zero value + if no_format: + if sign == 1: + return self._comp[ind] + else: # sign = -1 + return -self._comp[ind] + elif format_type is None: + if sign == 1: + return self._output_formatter(self._comp[ind]) + else: # sign = -1 + return self._output_formatter(-self._comp[ind]) + else: + if sign == 1: + return self._output_formatter( + self._comp[ind], format_type) + else: # sign = -1 + return self._output_formatter( + -self._comp[ind], format_type) + + def __setitem__(self, args, value): + r""" + Sets the component corresponding to the given indices. + + INPUT: + + - ``args`` -- list of indices (possibly a single integer if + self is a 1-index object) ; if ``[:]`` is provided, all the + components are set + - ``value`` -- the value to be set or a list of values if + ``args = [:]`` + + EXAMPLES:: + + sage: from sage.tensor.modules.comp import CompWithSym + sage: c = CompWithSym(ZZ, [1,2,3], 2, sym=(0,1)) + sage: c.__setitem__((1,2), 5) + sage: c[:] + [0 0 0] + [0 0 5] + [0 5 0] + sage: c = CompWithSym(ZZ, [1,2,3], 2, antisym=(0,1)) + sage: c.__setitem__((1,2), 5) + sage: c[:] + [ 0 0 0] + [ 0 0 5] + [ 0 -5 0] + sage: c.__setitem__((2,2), 5) + Traceback (most recent call last): + ... + ValueError: by antisymmetry, the component cannot have a nonzero value for the indices (2, 2) + + """ + format_type = None # default value, possibly redefined below + if isinstance(args, list): # case of [[...]] syntax + if isinstance(args[0], slice): + indices = args[0] + elif isinstance(args[0], (tuple, list)): # to ensure equivalence between + indices = args[0] # [[(i,j,...)]] or [[[i,j,...]]] and [[i,j,...]] + else: + indices = tuple(args) + else: + # Determining from the input the list of indices and the format + if isinstance(args, (int, Integer, slice)): + indices = args + elif isinstance(args[0], slice): + indices = args[0] + if len(args) == 2: + format_type = args[1] + elif len(args) == self._nid: + indices = args + else: + format_type = args[-1] + indices = args[:-1] + if isinstance(indices, slice): + self._set_list(indices, format_type, value) + else: + sign, ind = self._ordered_indices(indices) + if sign == 0: + if value != 0: + raise ValueError("by antisymmetry, the component cannot " + + "have a nonzero value for the indices " + + str(indices)) + if ind in self._comp: + del self._comp[ind] # zero values are not stored + elif value == 0: + if ind in self._comp: + del self._comp[ind] # zero values are not stored + else: + if format_type is None: + if sign == 1: + self._comp[ind] = self._ring(value) + else: # sign = -1 + self._comp[ind] = -self._ring(value) + else: + if sign == 1: + self._comp[ind] = self._ring({format_type: value}) + else: # sign = -1 + self._comp[ind] = -self._ring({format_type: value}) + + def swap_adjacent_indices(self, pos1, pos2, pos3): + r""" + Swap two adjacent sets of indices. + + This method is essentially required to reorder the covariant and + contravariant indices in the computation of a tensor product. + + The symmetries are preserved and the corresponding indices are adjusted + consequently. + + INPUT: + + - ``pos1`` -- position of the first index of set 1 (with the convention + position=0 for the first slot) + - ``pos2`` -- position of the first index of set 2 = 1 + position of + the last index of set 1 (since the two sets are adjacent) + - ``pos3`` -- 1 + position of the last index of set 2 + + OUTPUT: + + - Components with index set 1 permuted with index set 2. + + EXAMPLES: + + Swap of the index in position 0 with the pair of indices in position + (1,2) in a set of components antisymmetric with respect to the indices + in position (1,2):: + + sage: from sage.tensor.modules.comp import CompWithSym + sage: V = VectorSpace(QQ, 3) + sage: c = CompWithSym(QQ, V.basis(), 3, antisym=(1,2)) + sage: c[0,0,1], c[0,0,2], c[0,1,2] = (1,2,3) + sage: c[1,0,1], c[1,0,2], c[1,1,2] = (4,5,6) + sage: c[2,0,1], c[2,0,2], c[2,1,2] = (7,8,9) + sage: c[:] + [[[0, 1, 2], [-1, 0, 3], [-2, -3, 0]], + [[0, 4, 5], [-4, 0, 6], [-5, -6, 0]], + [[0, 7, 8], [-7, 0, 9], [-8, -9, 0]]] + sage: c1 = c.swap_adjacent_indices(0,1,3) + sage: c._antisym # c is antisymmetric with respect to the last pair of indices... + [(1, 2)] + sage: c1._antisym #...while c1 is antisymmetric with respect to the first pair of indices + [(0, 1)] + sage: c[0,1,2] + 3 + sage: c1[1,2,0] + 3 + sage: c1[2,1,0] + -3 + + """ + result = self._new_instance() + # The symmetries: + lpos = range(self._nid) + new_lpos = lpos[:pos1] + lpos[pos2:pos3] + lpos[pos1:pos2] + lpos[pos3:] + result._sym = [] + for s in self._sym: + new_s = [new_lpos.index(pos) for pos in s] + result._sym.append(tuple(sorted(new_s))) + result._antisym = [] + for s in self._antisym: + new_s = [new_lpos.index(pos) for pos in s] + result._antisym.append(tuple(sorted(new_s))) + # The values: + for ind, val in self._comp.iteritems(): + new_ind = ind[:pos1] + ind[pos2:pos3] + ind[pos1:pos2] + ind[pos3:] + result[new_ind] = val + return result + + def __add__(self, other): + r""" + Component addition. + + INPUT: + + - ``other`` -- components of the same number of indices and defined + on the same frame as ``self`` + + OUTPUT: + + - components resulting from the addition of ``self`` and ``other`` + + EXAMPLES:: + + sage: from sage.tensor.modules.comp import CompWithSym + sage: a = CompWithSym(ZZ, [1,2,3], 2, sym=(0,1)) + sage: a[0,1], a[1,2] = 4, 5 + sage: b = CompWithSym(ZZ, [1,2,3], 2, sym=(0,1)) + sage: b[0,1], b[2,2] = 2, -3 + sage: s = a.__add__(b) ; s # the symmetry is kept + 2-indices components w.r.t. [1, 2, 3], with symmetry on the index positions (0, 1) + sage: s[:] + [ 0 6 0] + [ 6 0 5] + [ 0 5 -3] + sage: s == a + b + True + sage: c = CompWithSym(ZZ, [1,2,3], 2, antisym=(0,1)) + sage: c[0,1], c[0,2] = 3, 7 + sage: s = a.__add__(c) ; s # the symmetry is lost + 2-indices components w.r.t. [1, 2, 3] + sage: s[:] + [ 0 7 7] + [ 1 0 5] + [-7 5 0] + + """ + if other == 0: + return +self + if not isinstance(other, Components): + raise TypeError("the second argument for the addition must be a " + + "an instance of Components") + if other._frame != self._frame: + raise ValueError("the two sets of components are not defined on " + + "the same frame") + if other._nid != self._nid: + raise ValueError("the two sets of components do not have the " + + "same number of indices") + if other._sindex != self._sindex: + raise ValueError("the two sets of components do not have the " + + "same starting index") + if isinstance(other, CompWithSym): + # Are the symmetries of the same type ? + diff_sym = set(self._sym).symmetric_difference(set(other._sym)) + diff_antisym = \ + set(self._antisym).symmetric_difference(set(other._antisym)) + if diff_sym == set() and diff_antisym == set(): + # The symmetries/antisymmetries are identical: + result = self.copy() + for ind, val in other._comp.iteritems(): + result[[ind]] += val + return result + else: + # The symmetries/antisymmetries are different: only the + # common ones are kept + common_sym = [] + for isym in self._sym: + for osym in other._sym: + com = tuple(set(isym).intersection(set(osym))) + if len(com) > 1: + common_sym.append(com) + common_antisym = [] + for isym in self._antisym: + for osym in other._antisym: + com = tuple(set(isym).intersection(set(osym))) + if len(com) > 1: + common_antisym.append(com) + if common_sym != [] or common_antisym != []: + result = CompWithSym(self._ring, self._frame, self._nid, + self._sindex, self._output_formatter, + common_sym, common_antisym) + else: + # no common symmetry -> the result is a generic Components: + result = Components(self._ring, self._frame, self._nid, + self._sindex, self._output_formatter) + else: + # other has no symmetry at all: + result = Components(self._ring, self._frame, self._nid, + self._sindex, self._output_formatter) + for ind in result.non_redundant_index_generator(): + result[[ind]] = self[[ind]] + other[[ind]] + return result + + + def __mul__(self, other): + r""" + Component tensor product. + + INPUT: + + - ``other`` -- components, on the same frame as ``self`` + + OUTPUT: + + - the tensor product of ``self`` by ``other`` + + EXAMPLES:: + + sage: from sage.tensor.modules.comp import CompWithSym + sage: a = CompWithSym(ZZ, [1,2,3], 2, sym=(0,1)) + sage: a[0,1], a[1,2] = 4, 5 + sage: b = CompWithSym(ZZ, [1,2,3], 2, sym=(0,1)) + sage: b[0,1], b[2,2] = 2, -3 + sage: s = a.__mul__(b) ; s + 4-indices components w.r.t. [1, 2, 3], with symmetry on the index positions (0, 1), with symmetry on the index positions (2, 3) + sage: s[1,0,0,1] + 8 + sage: s[1,0,0,1] == a[1,0] * b[0,1] + True + sage: s == a*b + True + sage: c = CompWithSym(ZZ, [1,2,3], 2, antisym=(0,1)) + sage: c[0,1], c[0,2] = 3, 7 + sage: s = a.__mul__(c) ; s + 4-indices components w.r.t. [1, 2, 3], with symmetry on the index positions (0, 1), with antisymmetry on the index positions (2, 3) + sage: s[1,0,2,0] + -28 + sage: s[1,0,2,0] == a[1,0] * c[2,0] + True + sage: s == a*c + True + + """ + if not isinstance(other, Components): + raise TypeError("the second argument for the tensor product " + + "be an instance of Components") + if other._frame != self._frame: + raise ValueError("the two sets of components are not defined on " + + "the same frame") + if other._sindex != self._sindex: + raise ValueError("the two sets of components do not have the " + + "same starting index") + sym = list(self._sym) + antisym = list(self._antisym) + if isinstance(other, CompWithSym): + if other._sym != []: + for s in other._sym: + ns = tuple(s[i]+self._nid for i in range(len(s))) + sym.append(ns) + if other._antisym != []: + for s in other._antisym: + ns = tuple(s[i]+self._nid for i in range(len(s))) + antisym.append(ns) + result = CompWithSym(self._ring, self._frame, self._nid + other._nid, + self._sindex, self._output_formatter, sym, antisym) + for ind_s, val_s in self._comp.iteritems(): + for ind_o, val_o in other._comp.iteritems(): + result._comp[ind_s + ind_o] = val_s * val_o + return result + + + def trace(self, pos1, pos2): + r""" + Index contraction, taking care of the symmetries. + + INPUT: + + - ``pos1`` -- position of the first index for the contraction (with + the convention position=0 for the first slot) + - ``pos2`` -- position of the second index for the contraction + + OUTPUT: + + - set of components resulting from the (pos1, pos2) contraction + + EXAMPLES: + + Self-contraction of symmetric 2-indices components:: + + sage: from sage.tensor.modules.comp import Components, CompWithSym, \ + ....: CompFullySym, CompFullyAntiSym + sage: V = VectorSpace(QQ, 3) + sage: a = CompFullySym(QQ, V.basis(), 2) + sage: a[:] = [[1,2,3],[2,4,5],[3,5,6]] + sage: a.trace(0,1) + 11 + sage: a[0,0] + a[1,1] + a[2,2] + 11 + + Self-contraction of antisymmetric 2-indices components:: + + sage: b = CompFullyAntiSym(QQ, V.basis(), 2) + sage: b[0,1], b[0,2], b[1,2] = (3, -2, 1) + sage: b.trace(0,1) # must be zero by antisymmetry + 0 + + Self-contraction of 3-indices components with one symmetry:: + + sage: v = Components(QQ, V.basis(), 1) + sage: v[:] = (-2, 4, -8) + sage: c = v*b ; c + 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with antisymmetry on the index positions (1, 2) + sage: s = c.trace(0,1) ; s + 1-index components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: s[:] + [-28, 2, 8] + sage: [sum(v[k]*b[k,i] for k in range(3)) for i in range(3)] # check + [-28, 2, 8] + sage: s = c.trace(1,2) ; s + 1-index components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: s[:] # is zero by antisymmetry + [0, 0, 0] + sage: c = b*v ; c + 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with antisymmetry on the index positions (0, 1) + sage: s = c.trace(0,1) + sage: s[:] # is zero by antisymmetry + [0, 0, 0] + sage: s = c.trace(1,2) ; s[:] + [28, -2, -8] + sage: [sum(b[i,k]*v[k] for k in range(3)) for i in range(3)] # check + [28, -2, -8] + + Self-contraction of 4-indices components with two symmetries:: + + sage: c = a*b ; c + 4-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with symmetry on the index positions (0, 1), with antisymmetry on the index positions (2, 3) + sage: s = c.trace(0,1) ; s # the symmetry on (0,1) is lost: + Fully antisymmetric 2-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: s[:] + [ 0 33 -22] + [-33 0 11] + [ 22 -11 0] + sage: [[sum(c[k,k,i,j] for k in range(3)) for j in range(3)] for i in range(3)] # check + [[0, 33, -22], [-33, 0, 11], [22, -11, 0]] + sage: s = c.trace(1,2) ; s # both symmetries are lost by this contraction + 2-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: s[:] + [ 0 0 0] + [-2 1 0] + [-3 3 -1] + sage: [[sum(c[i,k,k,j] for k in range(3)) for j in range(3)] for i in range(3)] # check + [[0, 0, 0], [-2, 1, 0], [-3, 3, -1]] + + """ + if self._nid < 2: + raise TypeError("contraction can be perfomed only on " + + "components with at least 2 indices") + if pos1 < 0 or pos1 > self._nid - 1: + raise IndexError("pos1 out of range") + if pos2 < 0 or pos2 > self._nid - 1: + raise IndexError("pos2 out of range") + if pos1 == pos2: + raise IndexError("the two positions must differ for the " + + "contraction to take place") + si = self._sindex + nsi = si + self._dim + if self._nid == 2: + res = 0 + for i in range(si, nsi): + res += self[[i,i]] + return res + else: + # More than 2 indices + if pos1 > pos2: + pos1, pos2 = (pos2, pos1) + # Determination of the remaining symmetries: + sym_res = list(self._sym) + for isym in self._sym: + isym_res = list(isym) + if pos1 in isym: + isym_res.remove(pos1) + if pos2 in isym: + isym_res.remove(pos2) + if len(isym_res) < 2: # the symmetry is lost + sym_res.remove(isym) + else: + sym_res[sym_res.index(isym)] = tuple(isym_res) + antisym_res = list(self._antisym) + for isym in self._antisym: + isym_res = list(isym) + if pos1 in isym: + isym_res.remove(pos1) + if pos2 in isym: + isym_res.remove(pos2) + if len(isym_res) < 2: # the symmetry is lost + antisym_res.remove(isym) + else: + antisym_res[antisym_res.index(isym)] = tuple(isym_res) + # Shift of the index positions to take into account the + # suppression of 2 indices: + max_sym = 0 + for k in range(len(sym_res)): + isym_res = [] + for pos in sym_res[k]: + if pos < pos1: + isym_res.append(pos) + elif pos < pos2: + isym_res.append(pos-1) + else: + isym_res.append(pos-2) + max_sym = max(max_sym, len(isym_res)) + sym_res[k] = tuple(isym_res) + max_antisym = 0 + for k in range(len(antisym_res)): + isym_res = [] + for pos in antisym_res[k]: + if pos < pos1: + isym_res.append(pos) + elif pos < pos2: + isym_res.append(pos-1) + else: + isym_res.append(pos-2) + max_antisym = max(max_antisym, len(isym_res)) + antisym_res[k] = tuple(isym_res) + # Construction of the appropriate object in view of the + # remaining symmetries: + nid_res = self._nid - 2 + if max_sym == 0 and max_antisym == 0: + result = Components(self._ring, self._frame, nid_res, self._sindex, + self._output_formatter) + elif max_sym == nid_res: + result = CompFullySym(self._ring, self._frame, nid_res, + self._sindex, self._output_formatter) + elif max_antisym == nid_res: + result = CompFullyAntiSym(self._ring, self._frame, nid_res, + self._sindex, self._output_formatter) + else: + result = CompWithSym(self._ring, self._frame, nid_res, + self._sindex, self._output_formatter, + sym=sym_res, antisym=antisym_res) + # The contraction itself: + for ind_res in result.non_redundant_index_generator(): + ind = list(ind_res) + ind.insert(pos1, 0) + ind.insert(pos2, 0) + res = 0 + for i in range(si, nsi): + ind[pos1] = i + ind[pos2] = i + res += self[[ind]] + result[[ind_res]] = res + return result + + + def non_redundant_index_generator(self): + r""" + Generator of indices, with only ordered indices in case of symmetries, + so that only non-redundant indices are generated. + + OUTPUT: + + - an iterable index + + EXAMPLES: + + Indices on a 2-dimensional space:: + + sage: from sage.tensor.modules.comp import Components, CompWithSym, \ + ... CompFullySym, CompFullyAntiSym + sage: V = VectorSpace(QQ, 2) + sage: c = CompFullySym(QQ, V.basis(), 2) + sage: for ind in c.non_redundant_index_generator(): print ind, + (0, 0) (0, 1) (1, 1) + sage: c = CompFullySym(QQ, V.basis(), 2, start_index=1) + sage: for ind in c.non_redundant_index_generator(): print ind, + (1, 1) (1, 2) (2, 2) + sage: c = CompFullyAntiSym(QQ, V.basis(), 2) + sage: for ind in c.non_redundant_index_generator(): print ind, + (0, 1) + + Indices on a 3-dimensional space:: + + sage: V = VectorSpace(QQ, 3) + sage: c = CompFullySym(QQ, V.basis(), 2) + sage: for ind in c.non_redundant_index_generator(): print ind, + (0, 0) (0, 1) (0, 2) (1, 1) (1, 2) (2, 2) + sage: c = CompFullySym(QQ, V.basis(), 2, start_index=1) + sage: for ind in c.non_redundant_index_generator(): print ind, + (1, 1) (1, 2) (1, 3) (2, 2) (2, 3) (3, 3) + sage: c = CompFullyAntiSym(QQ, V.basis(), 2) + sage: for ind in c.non_redundant_index_generator(): print ind, + (0, 1) (0, 2) (1, 2) + sage: c = CompWithSym(QQ, V.basis(), 3, sym=(1,2)) # symmetry on the last two indices + sage: for ind in c.non_redundant_index_generator(): print ind, + (0, 0, 0) (0, 0, 1) (0, 0, 2) (0, 1, 1) (0, 1, 2) (0, 2, 2) + (1, 0, 0) (1, 0, 1) (1, 0, 2) (1, 1, 1) (1, 1, 2) (1, 2, 2) + (2, 0, 0) (2, 0, 1) (2, 0, 2) (2, 1, 1) (2, 1, 2) (2, 2, 2) + sage: c = CompWithSym(QQ, V.basis(), 3, antisym=(1,2)) # antisymmetry on the last two indices + sage: for ind in c.non_redundant_index_generator(): print ind, + (0, 0, 1) (0, 0, 2) (0, 1, 2) (1, 0, 1) (1, 0, 2) (1, 1, 2) + (2, 0, 1) (2, 0, 2) (2, 1, 2) + sage: c = CompFullySym(QQ, V.basis(), 3) + sage: for ind in c.non_redundant_index_generator(): print ind, + (0, 0, 0) (0, 0, 1) (0, 0, 2) (0, 1, 1) (0, 1, 2) (0, 2, 2) + (1, 1, 1) (1, 1, 2) (1, 2, 2) (2, 2, 2) + sage: c = CompFullyAntiSym(QQ, V.basis(), 3) + sage: for ind in c.non_redundant_index_generator(): print ind, + (0, 1, 2) + + Indices on a 4-dimensional space:: + + sage: V = VectorSpace(QQ, 4) + sage: c = Components(QQ, V.basis(), 1) + sage: for ind in c.non_redundant_index_generator(): print ind, + (0,) (1,) (2,) (3,) + sage: c = CompFullyAntiSym(QQ, V.basis(), 2) + sage: for ind in c.non_redundant_index_generator(): print ind, + (0, 1) (0, 2) (0, 3) (1, 2) (1, 3) (2, 3) + sage: c = CompFullyAntiSym(QQ, V.basis(), 3) + sage: for ind in c.non_redundant_index_generator(): print ind, + (0, 1, 2) (0, 1, 3) (0, 2, 3) (1, 2, 3) + sage: c = CompFullyAntiSym(QQ, V.basis(), 4) + sage: for ind in c.non_redundant_index_generator(): print ind, + (0, 1, 2, 3) + sage: c = CompFullyAntiSym(QQ, V.basis(), 5) + sage: for ind in c.non_redundant_index_generator(): print ind, # nothing since c is identically zero in this case (for 5 > 4) + + """ + si = self._sindex + imax = self._dim - 1 + si + ind = [si for k in range(self._nid)] + ind_end = [si for k in range(self._nid)] + ind_end[0] = imax+1 + while ind != ind_end: + ordered = True + for isym in self._sym: + for k in range(len(isym)-1): + if ind[isym[k+1]] < ind[isym[k]]: + ordered = False + break + for isym in self._antisym: + for k in range(len(isym)-1): + if ind[isym[k+1]] <= ind[isym[k]]: + ordered = False + break + if ordered: + yield tuple(ind) + ret = 1 + for pos in range(self._nid-1,-1,-1): + if ind[pos] != imax: + ind[pos] += ret + ret = 0 + elif ret == 1: + if pos == 0: + ind[pos] = imax + 1 # end point reached + else: + ind[pos] = si + ret = 1 + + def symmetrize(self, *pos): + r""" + Symmetrization over the given index positions. + + INPUT: + + - ``pos`` -- list of index positions involved in the + symmetrization (with the convention ``position=0`` for the first + slot); if none, the symmetrization is performed over all the indices + + OUTPUT: + + - an instance of :class:`CompWithSym` describing the symmetrized + components + + EXAMPLES: + + Symmetrization of 3-indices components on a 3-dimensional space:: + + sage: from sage.tensor.modules.comp import Components, CompWithSym, \ + ....: CompFullySym, CompFullyAntiSym + sage: V = VectorSpace(QQ, 3) + sage: c = Components(QQ, V.basis(), 3) + sage: c[:] = [[[1,2,3], [4,5,6], [7,8,9]], [[10,11,12], [13,14,15], [16,17,18]], [[19,20,21], [22,23,24], [25,26,27]]] + sage: cs = c.symmetrize(0,1) ; cs + 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with symmetry on the index positions (0, 1) + sage: s = cs.symmetrize() ; s + Fully symmetric 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: cs[:], s[:] + ([[[1, 2, 3], [7, 8, 9], [13, 14, 15]], + [[7, 8, 9], [13, 14, 15], [19, 20, 21]], + [[13, 14, 15], [19, 20, 21], [25, 26, 27]]], + [[[1, 16/3, 29/3], [16/3, 29/3, 14], [29/3, 14, 55/3]], + [[16/3, 29/3, 14], [29/3, 14, 55/3], [14, 55/3, 68/3]], + [[29/3, 14, 55/3], [14, 55/3, 68/3], [55/3, 68/3, 27]]]) + sage: s == c.symmetrize() # should be true + True + sage: s1 = cs.symmetrize(0,1) ; s1 # should return a copy of cs + 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with symmetry on the index positions (0, 1) + sage: s1 == cs # check that s1 is a copy of cs + True + + Let us now start with a symmetry on the last two indices:: + + sage: cs1 = c.symmetrize(1,2) ; cs1 + 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with symmetry on the index positions (1, 2) + sage: s2 = cs1.symmetrize() ; s2 + Fully symmetric 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: s2 == c.symmetrize() + True + + Symmetrization alters pre-existing symmetries: let us symmetrize w.r.t. + the index positions `(1, 2)` a set of components that is symmetric + w.r.t. the index positions `(0, 1)`:: + + sage: cs = c.symmetrize(0,1) ; cs + 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with symmetry on the index positions (0, 1) + sage: css = cs.symmetrize(1,2) + sage: css # the symmetry (0,1) has been lost: + 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with symmetry on the index positions (1, 2) + sage: css[:] + [[[1, 9/2, 8], [9/2, 8, 23/2], [8, 23/2, 15]], + [[7, 21/2, 14], [21/2, 14, 35/2], [14, 35/2, 21]], + [[13, 33/2, 20], [33/2, 20, 47/2], [20, 47/2, 27]]] + sage: cs[:] + [[[1, 2, 3], [7, 8, 9], [13, 14, 15]], + [[7, 8, 9], [13, 14, 15], [19, 20, 21]], + [[13, 14, 15], [19, 20, 21], [25, 26, 27]]] + sage: css == c.symmetrize() # css differs from the full symmetrized version + False + sage: css.symmetrize() == c.symmetrize() # one has to symmetrize css over all indices to recover it + True + + Another example of symmetry alteration: symmetrization over `(0, 1)` of + a 4-indices set of components that is symmetric w.r.t. `(1, 2, 3)`:: + + sage: v = Components(QQ, V.basis(), 1) + sage: v[:] = (-2,1,4) + sage: a = v*s ; a + 4-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with symmetry on the index positions (1, 2, 3) + sage: a1 = a.symmetrize(0,1) ; a1 # the symmetry (1,2,3) has been reduced to (2,3): + 4-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with symmetry on the index positions (0, 1), with symmetry on the index positions (2, 3) + sage: a1._sym # a1 has two distinct symmetries: + [(0, 1), (2, 3)] + sage: a[0,1,2,0] == a[0,0,2,1] # a is symmetric w.r.t. positions 1 and 3 + True + sage: a1[0,1,2,0] == a1[0,0,2,1] # a1 is not + False + sage: a1[0,1,2,0] == a1[1,0,2,0] # but it is symmetric w.r.t. position 0 and 1 + True + sage: a[0,1,2,0] == a[1,0,2,0] # while a is not + False + + Partial symmetrization of 4-indices components with an antisymmetry on + the last two indices:: + + sage: a = Components(QQ, V.basis(), 2) + sage: a[:] = [[-1,2,3], [4,5,-6], [7,8,9]] + sage: b = CompFullyAntiSym(QQ, V.basis(), 2) + sage: b[0,1], b[0,2], b[1,2] = (2, 4, 8) + sage: c = a*b ; c + 4-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with antisymmetry on the index positions (2, 3) + sage: s = c.symmetrize(0,1) ; s # symmetrization on the first two indices + 4-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with symmetry on the index positions (0, 1), with antisymmetry on the index positions (2, 3) + sage: s[0,1,2,1] == (c[0,1,2,1] + c[1,0,2,1]) / 2 # check of the symmetrization + True + sage: s = c.symmetrize() ; s # symmetrization over all the indices + Fully symmetric 4-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: s == 0 # the full symmetrization results in zero due to the antisymmetry on the last two indices + True + sage: s = c.symmetrize(2,3) ; s + 4-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with symmetry on the index positions (2, 3) + sage: s == 0 # must be zero since the symmetrization has been performed on the antisymmetric indices + True + sage: s = c.symmetrize(0,2) ; s + 4-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with symmetry on the index positions (0, 2) + sage: s != 0 # s is not zero, but the antisymmetry on (2,3) is lost because the position 2 is involved in the new symmetry + True + + Partial symmetrization of 4-indices components with an antisymmetry on + the last three indices:: + + sage: a = Components(QQ, V.basis(), 1) + sage: a[:] = (1, -2, 3) + sage: b = CompFullyAntiSym(QQ, V.basis(), 3) + sage: b[0,1,2] = 4 + sage: c = a*b ; c + 4-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with antisymmetry on the index positions (1, 2, 3) + sage: s = c.symmetrize(0,1) ; s + 4-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with symmetry on the index positions (0, 1), + with antisymmetry on the index positions (2, 3) + + Note that the antisymmetry on `(1, 2, 3)` has been reduced to + `(2, 3)` only:: + + sage: s = c.symmetrize(1,2) ; s + 4-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with symmetry on the index positions (1, 2) + sage: s == 0 # because (1,2) are involved in the original antisymmetry + True + + """ + from sage.groups.perm_gps.permgroup_named import SymmetricGroup + if not pos: + pos = range(self._nid) + else: + if len(pos) < 2: + raise ValueError("at least two index positions must be given") + if len(pos) > self._nid: + raise ValueError("number of index positions larger than the " \ + "total number of indices") + pos = tuple(pos) + pos_set = set(pos) + # If the symmetry is already present, there is nothing to do: + for isym in self._sym: + if pos_set.issubset(set(isym)): + return self.copy() + # + # Interference of the new symmetry with existing ones: + # + sym_res = [pos] # starting the list of symmetries of the result + for isym in self._sym: + inter = pos_set.intersection(set(isym)) + # if len(inter) == len(isym), isym is included in the new symmetry + # and therefore has not to be included in sym_res + if len(inter) != len(isym): + if len(inter) >= 1: + # some part of isym is lost + isym_set = set(isym) + for k in inter: + isym_set.remove(k) + if len(isym_set) > 1: + # some part of isym remains and must be included in sym_res: + isym_res = tuple(isym_set) + sym_res.append(isym_res) + else: + # case len(inter)=0: no interference: the existing symmetry is + # added to the list of symmetries for the result: + sym_res.append(isym) + # + # Interference of the new symmetry with existing antisymmetries: + # + antisym_res = [] # starting the list of antisymmetries of the result + zero_result = False + for iasym in self._antisym: + inter = pos_set.intersection(set(iasym)) + if len(inter) > 1: + # If at least two of the symmetry indices are already involved + # in the antisymmetry, the outcome is zero: + zero_result = True + elif len(inter) == 1: + # some piece of antisymmetry is lost + k = inter.pop() # the symmetry index position involved in the + # antisymmetry + iasym_set = set(iasym) + iasym_set.remove(k) + if len(iasym_set) > 1: + iasym_res = tuple(iasym_set) + antisym_res.append(iasym_res) + # if len(iasym_set) == 1, the antisymmetry is fully lost, it is + # therefore not appended to antisym_res + else: + # case len(inter)=0: no interference: the antisymmetry is + # added to the list of antisymmetries for the result: + antisym_res.append(iasym) + # + # Creation of the result object + # + max_sym = 0 + for isym in sym_res: + max_sym = max(max_sym, len(isym)) + if max_sym == self._nid: + result = CompFullySym(self._ring, self._frame, self._nid, self._sindex, + self._output_formatter) + else: + result = CompWithSym(self._ring, self._frame, self._nid, self._sindex, + self._output_formatter, sym=sym_res, + antisym=antisym_res) + if zero_result: + return result # since a just created instance is zero + # + # Symmetrization + # + n_sym = len(pos) # number of indices involved in the symmetry + sym_group = SymmetricGroup(n_sym) + for ind in result.non_redundant_index_generator(): + sum = 0 + for perm in sym_group.list(): + # action of the permutation on [0,1,...,n_sym-1]: + perm_action = map(lambda x: x-1, perm.domain()) + ind_perm = list(ind) + for k in range(n_sym): + ind_perm[pos[perm_action[k]]] = ind[pos[k]] + sum += self[[ind_perm]] + result[[ind]] = sum / sym_group.order() + return result + + + def antisymmetrize(self, *pos): + r""" + Antisymmetrization over the given index positions. + + INPUT: + + - ``pos`` -- list of index positions involved in the antisymmetrization + (with the convention ``position=0`` for the first slot); if none, the + antisymmetrization is performed over all the indices + + OUTPUT: + + - an instance of :class:`CompWithSym` describing the antisymmetrized + components + + EXAMPLES: + + Antisymmetrization of 3-indices components on a 3-dimensional space:: + + sage: from sage.tensor.modules.comp import Components, CompWithSym, \ + ... CompFullySym, CompFullyAntiSym + sage: V = VectorSpace(QQ, 3) + sage: a = Components(QQ, V.basis(), 1) + sage: a[:] = (-2,1,3) + sage: b = CompFullyAntiSym(QQ, V.basis(), 2) + sage: b[0,1], b[0,2], b[1,2] = (4,1,2) + sage: c = a*b ; c # tensor product of a by b + 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with antisymmetry on the index positions (1, 2) + sage: s = c.antisymmetrize() ; s + Fully antisymmetric 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: c[:], s[:] + ([[[0, -8, -2], [8, 0, -4], [2, 4, 0]], + [[0, 4, 1], [-4, 0, 2], [-1, -2, 0]], + [[0, 12, 3], [-12, 0, 6], [-3, -6, 0]]], + [[[0, 0, 0], [0, 0, 7/3], [0, -7/3, 0]], + [[0, 0, -7/3], [0, 0, 0], [7/3, 0, 0]], + [[0, 7/3, 0], [-7/3, 0, 0], [0, 0, 0]]]) + + Check of the antisymmetrization:: + + sage: all(s[i,j,k] == (c[i,j,k]-c[i,k,j]+c[j,k,i]-c[j,i,k]+c[k,i,j]-c[k,j,i])/6 + ....: for i in range(3) for j in range(3) for k in range(3)) + True + + Antisymmetrization over already antisymmetric indices does not change anything:: + + sage: s1 = s.antisymmetrize(1,2) ; s1 + Fully antisymmetric 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: s1 == s + True + sage: c1 = c.antisymmetrize(1,2) ; c1 + 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with antisymmetry on the index positions (1, 2) + sage: c1 == c + True + + But in general, antisymmetrization may alter previous antisymmetries:: + + sage: c2 = c.antisymmetrize(0,1) ; c2 # the antisymmetry (2,3) is lost: + 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with antisymmetry on the index positions (0, 1) + sage: c2 == c + False + sage: c = s*a ; c + 4-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with antisymmetry on the index positions (0, 1, 2) + sage: s = c.antisymmetrize(1,3) ; s + 4-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with antisymmetry on the index positions (1, 3), + with antisymmetry on the index positions (0, 2) + sage: s._antisym # the antisymmetry (0,1,2) has been reduced to (0,2), since 1 is involved in the new antisymmetry (1,3): + [(1, 3), (0, 2)] + + Partial antisymmetrization of 4-indices components with a symmetry on + the first two indices:: + + sage: a = CompFullySym(QQ, V.basis(), 2) + sage: a[:] = [[-2,1,3], [1,0,-5], [3,-5,4]] + sage: b = Components(QQ, V.basis(), 2) + sage: b[:] = [[1,2,3], [5,7,11], [13,17,19]] + sage: c = a*b ; c + 4-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with symmetry on the index positions (0, 1) + sage: s = c.antisymmetrize(2,3) ; s + 4-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with symmetry on the index positions (0, 1), + with antisymmetry on the index positions (2, 3) + + Some check of the antisymmetrization:: + + sage: for i in range(3): + ....: for j in range(i,3): + ....: print (s[2,2,i,j], s[2,2,i,j] == (c[2,2,i,j] - c[2,2,j,i])/2), + (0, True) (-6, True) (-20, True) (0, True) (-12, True) (0, True) + + The full antisymmetrization results in zero because of the symmetry on the + first two indices:: + + sage: s = c.antisymmetrize() ; s + Fully antisymmetric 4-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: s == 0 + True + + Similarly, the partial antisymmetrization on the first two indices results in zero:: + + sage: s = c.antisymmetrize(0,1) ; s + 4-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with antisymmetry on the index positions (0, 1) + sage: s == 0 + True + + The partial antisymmetrization on the positions `(0, 2)` destroys + the symmetry on `(0, 1)`:: + + sage: s = c.antisymmetrize(0,2) ; s + 4-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with antisymmetry on the index positions (0, 2) + sage: s != 0 + True + sage: s[0,1,2,1] + 27/2 + sage: s[1,0,2,1] # the symmetry (0,1) is lost + -2 + sage: s[2,1,0,1] # the antisymmetry (0,2) holds + -27/2 + + """ + from sage.groups.perm_gps.permgroup_named import SymmetricGroup + if not pos: + pos = range(self._nid) + else: + if len(pos) < 2: + raise ValueError("at least two index positions must be given") + if len(pos) > self._nid: + raise ValueError("number of index positions larger than the " \ + "total number of indices") + pos = tuple(pos) + pos_set = set(pos) + # If the antisymmetry is already present, there is nothing to do: + for iasym in self._antisym: + if pos_set.issubset(set(iasym)): + return self.copy() + # + # Interference of the new antisymmetry with existing ones + # + antisym_res = [pos] # starting the list of symmetries of the result + for iasym in self._antisym: + inter = pos_set.intersection(set(iasym)) + # if len(inter) == len(iasym), iasym is included in the new + # antisymmetry and therefore has not to be included in antisym_res + if len(inter) != len(iasym): + if len(inter) >= 1: + # some part of iasym is lost + iasym_set = set(iasym) + for k in inter: + iasym_set.remove(k) + if len(iasym_set) > 1: + # some part of iasym remains and must be included in + # antisym_res: + iasym_res = tuple(iasym_set) + antisym_res.append(iasym_res) + else: + # case len(inter)=0: no interference: the existing + # antisymmetry is added to the list of antisymmetries for + # the result: + antisym_res.append(iasym) + # + # Interference of the new antisymmetry with existing symmetries + # + sym_res = [] # starting the list of symmetries of the result + zero_result = False + for isym in self._sym: + inter = pos_set.intersection(set(isym)) + if len(inter) > 1: + # If at least two of the antisymmetry indices are already + # involved in the symmetry, the outcome is zero: + zero_result = True + elif len(inter) == 1: + # some piece of the symmetry is lost + k = inter.pop() # the antisymmetry index position involved in + # the symmetry + isym_set = set(isym) + isym_set.remove(k) + if len(isym_set) > 1: + isym_res = tuple(isym_set) + sym_res.append(isym_res) + # if len(isym_set) == 1, the symmetry is fully lost, it is + # therefore not appended to sym_res + else: + # case len(inter)=0: no interference: the symmetry is + # added to the list of symmetries for the result: + sym_res.append(isym) + # + # Creation of the result object + # + max_sym = 0 + for isym in antisym_res: + max_sym = max(max_sym, len(isym)) + if max_sym == self._nid: + result = CompFullyAntiSym(self._ring, self._frame, self._nid, + self._sindex, self._output_formatter) + else: + result = CompWithSym(self._ring, self._frame, self._nid, self._sindex, + self._output_formatter, sym=sym_res, + antisym=antisym_res) + if zero_result: + return result # since a just created instance is zero + # + # Antisymmetrization + # + n_sym = len(pos) # number of indices involved in the antisymmetry + sym_group = SymmetricGroup(n_sym) + for ind in result.non_redundant_index_generator(): + sum = 0 + for perm in sym_group.list(): + # action of the permutation on [0,1,...,n_sym-1]: + perm_action = map(lambda x: x-1, perm.domain()) + ind_perm = list(ind) + for k in range(n_sym): + ind_perm[pos[perm_action[k]]] = ind[pos[k]] + if perm.sign() == 1: + sum += self[[ind_perm]] + else: + sum -= self[[ind_perm]] + result[[ind]] = sum / sym_group.order() + return result + + +#****************************************************************************** + +class CompFullySym(CompWithSym): + r""" + Indexed set of ring elements forming some components with respect to a + given "frame" that are fully symmetric with respect to any permutation + of the indices. + + The "frame" can be a basis of some vector space or a vector frame on some + manifold (i.e. a field of bases). + The stored quantities can be tensor components or non-tensorial quantities. + + INPUT: + + - ``ring`` -- commutative ring in which each component takes its value + - ``frame`` -- frame with respect to which the components are defined; + whatever type ``frame`` is, it should have some method ``__len__()`` + implemented, so that ``len(frame)`` returns the dimension, i.e. the size + of a single index range + - ``nb_indices`` -- number of indices labeling the components + - ``start_index`` -- (default: 0) first value of a single index; + accordingly a component index i must obey + ``start_index <= i <= start_index + dim - 1``, where ``dim = len(frame)``. + - ``output_formatter`` -- (default: ``None``) function or unbound + method called to format the output of the component access + operator ``[...]`` (method __getitem__); ``output_formatter`` must take + 1 or 2 arguments: the 1st argument must be an instance of ``ring`` and + the second one, if any, some format specification. + + EXAMPLES: + + Symmetric components with 2 indices on a 3-dimensional space:: + + sage: from sage.tensor.modules.comp import CompFullySym, CompWithSym + sage: V = VectorSpace(QQ, 3) + sage: c = CompFullySym(QQ, V.basis(), 2) + sage: c[0,0], c[0,1], c[1,2] = 1, -2, 3 + sage: c[:] # note that c[1,0] and c[2,1] have been updated automatically (by symmetry) + [ 1 -2 0] + [-2 0 3] + [ 0 3 0] + + Internally, only non-redundant and non-zero components are stored:: + + sage: c._comp # random output order of the component dictionary + {(0, 0): 1, (0, 1): -2, (1, 2): 3} + + Same thing, but with the starting index set to 1:: + + sage: c1 = CompFullySym(QQ, V.basis(), 2, start_index=1) + sage: c1[1,1], c1[1,2], c1[2,3] = 1, -2, 3 + sage: c1[:] + [ 1 -2 0] + [-2 0 3] + [ 0 3 0] + + The values stored in ``c`` and ``c1`` are equal:: + + sage: c1[:] == c[:] + True + + but not ``c`` and ``c1``, since their starting indices differ:: + + sage: c1 == c + False + + Fully symmetric components with 3 indices on a 3-dimensional space:: + + sage: a = CompFullySym(QQ, V.basis(), 3) + sage: a[0,1,2] = 3 + sage: a[:] + [[[0, 0, 0], [0, 0, 3], [0, 3, 0]], + [[0, 0, 3], [0, 0, 0], [3, 0, 0]], + [[0, 3, 0], [3, 0, 0], [0, 0, 0]]] + sage: a[0,1,0] = 4 + sage: a[:] + [[[0, 4, 0], [4, 0, 3], [0, 3, 0]], + [[4, 0, 3], [0, 0, 0], [3, 0, 0]], + [[0, 3, 0], [3, 0, 0], [0, 0, 0]]] + + The full symmetry is preserved by the arithmetics:: + + sage: b = CompFullySym(QQ, V.basis(), 3) + sage: b[0,0,0], b[0,1,0], b[1,0,2], b[1,2,2] = -2, 3, 1, -5 + sage: s = a + 2*b ; s + Fully symmetric 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: a[:], b[:], s[:] + ([[[0, 4, 0], [4, 0, 3], [0, 3, 0]], + [[4, 0, 3], [0, 0, 0], [3, 0, 0]], + [[0, 3, 0], [3, 0, 0], [0, 0, 0]]], + [[[-2, 3, 0], [3, 0, 1], [0, 1, 0]], + [[3, 0, 1], [0, 0, 0], [1, 0, -5]], + [[0, 1, 0], [1, 0, -5], [0, -5, 0]]], + [[[-4, 10, 0], [10, 0, 5], [0, 5, 0]], + [[10, 0, 5], [0, 0, 0], [5, 0, -10]], + [[0, 5, 0], [5, 0, -10], [0, -10, 0]]]) + + It is lost if the added object is not fully symmetric:: + + sage: b1 = CompWithSym(QQ, V.basis(), 3, sym=(0,1)) # b1 has only symmetry on index positions (0,1) + sage: b1[0,0,0], b1[0,1,0], b1[1,0,2], b1[1,2,2] = -2, 3, 1, -5 + sage: s = a + 2*b1 ; s # the result has the same symmetry as b1: + 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with symmetry on the index positions (0, 1) + sage: a[:], b1[:], s[:] + ([[[0, 4, 0], [4, 0, 3], [0, 3, 0]], + [[4, 0, 3], [0, 0, 0], [3, 0, 0]], + [[0, 3, 0], [3, 0, 0], [0, 0, 0]]], + [[[-2, 0, 0], [3, 0, 1], [0, 0, 0]], + [[3, 0, 1], [0, 0, 0], [0, 0, -5]], + [[0, 0, 0], [0, 0, -5], [0, 0, 0]]], + [[[-4, 4, 0], [10, 0, 5], [0, 3, 0]], + [[10, 0, 5], [0, 0, 0], [3, 0, -10]], + [[0, 3, 0], [3, 0, -10], [0, 0, 0]]]) + sage: s = 2*b1 + a ; s + 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with symmetry on the index positions (0, 1) + sage: 2*b1 + a == a + 2*b1 + True + + """ + def __init__(self, ring, frame, nb_indices, start_index=0, + output_formatter=None): + r""" + TEST:: + + sage: from sage.tensor.modules.comp import CompFullySym + sage: C = CompFullySym(ZZ, (1,2,3), 2) + sage: TestSuite(C).run() + + """ + CompWithSym.__init__(self, ring, frame, nb_indices, start_index, + output_formatter, sym=range(nb_indices)) + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLE:: + + sage: from sage.tensor.modules.comp import CompFullySym + sage: CompFullySym(ZZ, (1,2,3), 4) + Fully symmetric 4-indices components w.r.t. (1, 2, 3) + + """ + return "Fully symmetric " + str(self._nid) + "-indices" + \ + " components w.r.t. " + str(self._frame) + + def _new_instance(self): + r""" + Creates a :class:`CompFullySym` instance w.r.t. the same frame, + and with the same number of indices. + + EXAMPLE:: + + sage: from sage.tensor.modules.comp import CompFullySym + sage: c = CompFullySym(ZZ, (1,2,3), 4) + sage: c._new_instance() + Fully symmetric 4-indices components w.r.t. (1, 2, 3) + + """ + return CompFullySym(self._ring, self._frame, self._nid, self._sindex, + self._output_formatter) + + def __getitem__(self, args): + r""" + Return the component corresponding to the given indices of ``self``. + + INPUT: + + - ``args`` -- list of indices (possibly a single integer if + self is a 1-index object) or the character ``:`` for the full list + of components + + OUTPUT: + + - the component corresponding to ``args`` or, if ``args`` = ``:``, + the full list of components, in the form ``T[i][j]...`` for the + components `T_{ij...}` (for a 2-indices object, a matrix is returned) + + EXAMPLES:: + + sage: from sage.tensor.modules.comp import CompFullySym + sage: c = CompFullySym(ZZ, (1,2,3), 2) + sage: c[0,1] = 4 + sage: c.__getitem__((0,1)) + 4 + sage: c.__getitem__((1,0)) + 4 + sage: c.__getitem__(slice(None)) + [0 4 0] + [4 0 0] + [0 0 0] + + """ + no_format = self._output_formatter is None + format_type = None # default value, possibly redefined below + if isinstance(args, list): # case of [[...]] syntax + no_format = True + if isinstance(args[0], slice): + indices = args[0] + elif isinstance(args[0], (tuple, list)): # to ensure equivalence between + indices = args[0] # [[(i,j,...)]] or [[[i,j,...]]] and [[i,j,...]] + else: + indices = tuple(args) + else: + # Determining from the input the list of indices and the format + if isinstance(args, (int, Integer, slice)): + indices = args + elif isinstance(args[0], slice): + indices = args[0] + if len(args) == 2: + format_type = args[1] + elif len(args) == self._nid: + indices = args + else: + format_type = args[-1] + indices = args[:-1] + + if isinstance(indices, slice): + return self._get_list(indices, no_format, format_type) + + ind = self._ordered_indices(indices)[1] # [0]=sign is not used + if ind in self._comp: # non zero value + if no_format: + return self._comp[ind] + elif format_type is None: + return self._output_formatter(self._comp[ind]) + else: + return self._output_formatter(self._comp[ind], format_type) + + # the value is zero + if no_format: + return self._ring.zero() + elif format_type is None: + return self._output_formatter(self._ring.zero()) + else: + return self._output_formatter(self._ring.zero(), + format_type) + + def __setitem__(self, args, value): + r""" + Sets the component corresponding to the given indices. + + INPUT: + + - ``indices`` -- list of indices (possibly a single integer if + self is a 1-index object) ; if [:] is provided, all the components + are set. + - ``value`` -- the value to be set or a list of values if + ``args = [:]`` + + EXAMPLES:: + + sage: from sage.tensor.modules.comp import CompFullySym + sage: c = CompFullySym(ZZ, (1,2,3), 2) + sage: c.__setitem__((0,1), 4) + sage: c[:] + [0 4 0] + [4 0 0] + [0 0 0] + sage: c.__setitem__((2,1), 5) + sage: c[:] + [0 4 0] + [4 0 5] + [0 5 0] + sage: c.__setitem__(slice(None), [[1, 2, 3], [2, 4, 5], [3, 5, 6]]) + sage: c[:] + [1 2 3] + [2 4 5] + [3 5 6] + + """ + format_type = None # default value, possibly redefined below + if isinstance(args, list): # case of [[...]] syntax + if isinstance(args[0], slice): + indices = args[0] + elif isinstance(args[0], (tuple, list)): # to ensure equivalence between + indices = args[0] # [[(i,j,...)]] or [[[i,j,...]]] and [[i,j,...]] + else: + indices = tuple(args) + else: + # Determining from the input the list of indices and the format + if isinstance(args, (int, Integer, slice)): + indices = args + elif isinstance(args[0], slice): + indices = args[0] + if len(args) == 2: + format_type = args[1] + elif len(args) == self._nid: + indices = args + else: + format_type = args[-1] + indices = args[:-1] + if isinstance(indices, slice): + self._set_list(indices, format_type, value) + else: + ind = self._ordered_indices(indices)[1] # [0]=sign is not used + if value == 0: + if ind in self._comp: + del self._comp[ind] # zero values are not stored + else: + if format_type is None: + self._comp[ind] = self._ring(value) + else: + self._comp[ind] = self._ring({format_type: value}) + + def __add__(self, other): + r""" + Component addition. + + INPUT: + + - ``other`` -- components of the same number of indices and defined + on the same frame as ``self`` + + OUTPUT: + + - components resulting from the addition of ``self`` and ``other`` + + EXAMPLES:: + + sage: from sage.tensor.modules.comp import CompFullySym + sage: a = CompFullySym(ZZ, (1,2,3), 2) + sage: a[0,1], a[1,2] = 4, 5 + sage: b = CompFullySym(ZZ, (1,2,3), 2) + sage: b[0,1], b[2,2] = 2, -3 + sage: s = a.__add__(b) ; s # the symmetry is kept + Fully symmetric 2-indices components w.r.t. (1, 2, 3) + sage: s[:] + [ 0 6 0] + [ 6 0 5] + [ 0 5 -3] + sage: s == a + b + True + sage: from sage.tensor.modules.comp import CompFullyAntiSym + sage: c = CompFullyAntiSym(ZZ, (1,2,3), 2) + sage: c[0,1], c[0,2] = 3, 7 + sage: s = a.__add__(c) ; s # the symmetry is lost + 2-indices components w.r.t. (1, 2, 3) + sage: s[:] + [ 0 7 7] + [ 1 0 5] + [-7 5 0] + sage: s == a + c + True + + """ + if other == 0: + return +self + if not isinstance(other, Components): + raise TypeError("the second argument for the addition must be a " + + "an instance of Components") + if isinstance(other, CompFullySym): + if other._frame != self._frame: + raise ValueError("the two sets of components are not defined " + + "on the same frame") + if other._nid != self._nid: + raise ValueError("the two sets of components do not have the " + + "same number of indices") + if other._sindex != self._sindex: + raise ValueError("the two sets of components do not have the " + + "same starting index") + result = self.copy() + for ind, val in other._comp.iteritems(): + result[[ind]] += val + return result + else: + return CompWithSym.__add__(self, other) + + +#****************************************************************************** + +class CompFullyAntiSym(CompWithSym): + r""" + Indexed set of ring elements forming some components with respect to a + given "frame" that are fully antisymmetric with respect to any permutation + of the indices. + + The "frame" can be a basis of some vector space or a vector frame on some + manifold (i.e. a field of bases). + The stored quantities can be tensor components or non-tensorial quantities. + + INPUT: + + - ``ring`` -- commutative ring in which each component takes its value + - ``frame`` -- frame with respect to which the components are defined; + whatever type ``frame`` is, it should have some method ``__len__()`` + implemented, so that ``len(frame)`` returns the dimension, i.e. the size + of a single index range + - ``nb_indices`` -- number of indices labeling the components + - ``start_index`` -- (default: 0) first value of a single index; + accordingly a component index i must obey + ``start_index <= i <= start_index + dim - 1``, where ``dim = len(frame)``. + - ``output_formatter`` -- (default: ``None``) function or unbound + method called to format the output of the component access + operator ``[...]`` (method __getitem__); ``output_formatter`` must take + 1 or 2 arguments: the 1st argument must be an instance of ``ring`` and + the second one, if any, some format specification. + + EXAMPLES: + + Antisymmetric components with 2 indices on a 3-dimensional space:: + + sage: from sage.tensor.modules.comp import CompWithSym, CompFullyAntiSym + sage: V = VectorSpace(QQ, 3) + sage: c = CompFullyAntiSym(QQ, V.basis(), 2) + sage: c[0,1], c[0,2], c[1,2] = 3, 1/2, -1 + sage: c[:] # note that all components have been set according to the antisymmetry + [ 0 3 1/2] + [ -3 0 -1] + [-1/2 1 0] + + Internally, only non-redundant and non-zero components are stored:: + + sage: c._comp # random output order of the component dictionary + {(0, 1): 3, (0, 2): 1/2, (1, 2): -1} + + Same thing, but with the starting index set to 1:: + + sage: c1 = CompFullyAntiSym(QQ, V.basis(), 2, start_index=1) + sage: c1[1,2], c1[1,3], c1[2,3] = 3, 1/2, -1 + sage: c1[:] + [ 0 3 1/2] + [ -3 0 -1] + [-1/2 1 0] + + The values stored in ``c`` and ``c1`` are equal:: + + sage: c1[:] == c[:] + True + + but not ``c`` and ``c1``, since their starting indices differ:: + + sage: c1 == c + False + + Fully antisymmetric components with 3 indices on a 3-dimensional space:: + + sage: a = CompFullyAntiSym(QQ, V.basis(), 3) + sage: a[0,1,2] = 3 # the only independent component in dimension 3 + sage: a[:] + [[[0, 0, 0], [0, 0, 3], [0, -3, 0]], + [[0, 0, -3], [0, 0, 0], [3, 0, 0]], + [[0, 3, 0], [-3, 0, 0], [0, 0, 0]]] + + Setting a nonzero value incompatible with the antisymmetry results in an + error:: + + sage: a[0,1,0] = 4 + Traceback (most recent call last): + ... + ValueError: by antisymmetry, the component cannot have a nonzero value for the indices (0, 1, 0) + sage: a[0,1,0] = 0 # OK + sage: a[2,0,1] = 3 # OK + + The full antisymmetry is preserved by the arithmetics:: + + sage: b = CompFullyAntiSym(QQ, V.basis(), 3) + sage: b[0,1,2] = -4 + sage: s = a + 2*b ; s + Fully antisymmetric 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: a[:], b[:], s[:] + ([[[0, 0, 0], [0, 0, 3], [0, -3, 0]], + [[0, 0, -3], [0, 0, 0], [3, 0, 0]], + [[0, 3, 0], [-3, 0, 0], [0, 0, 0]]], + [[[0, 0, 0], [0, 0, -4], [0, 4, 0]], + [[0, 0, 4], [0, 0, 0], [-4, 0, 0]], + [[0, -4, 0], [4, 0, 0], [0, 0, 0]]], + [[[0, 0, 0], [0, 0, -5], [0, 5, 0]], + [[0, 0, 5], [0, 0, 0], [-5, 0, 0]], + [[0, -5, 0], [5, 0, 0], [0, 0, 0]]]) + + It is lost if the added object is not fully antisymmetric:: + + sage: b1 = CompWithSym(QQ, V.basis(), 3, antisym=(0,1)) # b1 has only antisymmetry on index positions (0,1) + sage: b1[0,1,2] = -4 + sage: s = a + 2*b1 ; s # the result has the same symmetry as b1: + 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with antisymmetry on the index positions (0, 1) + sage: a[:], b1[:], s[:] + ([[[0, 0, 0], [0, 0, 3], [0, -3, 0]], + [[0, 0, -3], [0, 0, 0], [3, 0, 0]], + [[0, 3, 0], [-3, 0, 0], [0, 0, 0]]], + [[[0, 0, 0], [0, 0, -4], [0, 0, 0]], + [[0, 0, 4], [0, 0, 0], [0, 0, 0]], + [[0, 0, 0], [0, 0, 0], [0, 0, 0]]], + [[[0, 0, 0], [0, 0, -5], [0, -3, 0]], + [[0, 0, 5], [0, 0, 0], [3, 0, 0]], + [[0, 3, 0], [-3, 0, 0], [0, 0, 0]]]) + sage: s = 2*b1 + a ; s + 3-indices components w.r.t. [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ], with antisymmetry on the index positions (0, 1) + sage: 2*b1 + a == a + 2*b1 + True + + """ + def __init__(self, ring, frame, nb_indices, start_index=0, + output_formatter=None): + r""" + TESTS:: + + sage: from sage.tensor.modules.comp import CompFullyAntiSym + sage: C = CompFullyAntiSym(ZZ, (1,2,3), 2) + sage: TestSuite(C).run() + + """ + CompWithSym.__init__(self, ring, frame, nb_indices, start_index, + output_formatter, antisym=range(nb_indices)) + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLES:: + + sage: from sage.tensor.modules.comp import CompFullyAntiSym + sage: CompFullyAntiSym(ZZ, (1,2,3), 4) + Fully antisymmetric 4-indices components w.r.t. (1, 2, 3) + + """ + return "Fully antisymmetric " + str(self._nid) + "-indices" + \ + " components w.r.t. " + str(self._frame) + + def _new_instance(self): + r""" + Creates a :class:`CompFullyAntiSym` instance w.r.t. the same frame, + and with the same number of indices. + + EXAMPLE:: + + sage: from sage.tensor.modules.comp import CompFullyAntiSym + sage: c = CompFullyAntiSym(ZZ, (1,2,3), 4) + sage: c._new_instance() + Fully antisymmetric 4-indices components w.r.t. (1, 2, 3) + + """ + return CompFullyAntiSym(self._ring, self._frame, self._nid, self._sindex, + self._output_formatter) + + + def __add__(self, other): + r""" + Component addition. + + INPUT: + + - ``other`` -- components of the same number of indices and defined + on the same frame as ``self`` + + OUTPUT: + + - components resulting from the addition of ``self`` and ``other`` + + EXAMPLES:: + + sage: from sage.tensor.modules.comp import CompFullyAntiSym + sage: a = CompFullyAntiSym(ZZ, (1,2,3), 2) + sage: a[0,1], a[1,2] = 4, 5 + sage: b = CompFullyAntiSym(ZZ, (1,2,3), 2) + sage: b[0,1], b[0,2] = 2, -3 + sage: s = a.__add__(b) ; s # the antisymmetry is kept + Fully antisymmetric 2-indices components w.r.t. (1, 2, 3) + sage: s[:] + [ 0 6 -3] + [-6 0 5] + [ 3 -5 0] + sage: s == a + b + True + sage: from sage.tensor.modules.comp import CompFullySym + sage: c = CompFullySym(ZZ, (1,2,3), 2) + sage: c[0,1], c[0,2] = 3, 7 + sage: s = a.__add__(c) ; s # the antisymmetry is lost + 2-indices components w.r.t. (1, 2, 3) + sage: s[:] + [ 0 7 7] + [-1 0 5] + [ 7 -5 0] + sage: s == a + c + True + + """ + if other == 0: + return +self + if not isinstance(other, Components): + raise TypeError("the second argument for the addition must be a " + + "an instance of Components") + if isinstance(other, CompFullyAntiSym): + if other._frame != self._frame: + raise ValueError("the two sets of components are not defined " + + "on the same frame") + if other._nid != self._nid: + raise ValueError("the two sets of components do not have the " + + "same number of indices") + if other._sindex != self._sindex: + raise ValueError("the two sets of components do not have the " + + "same starting index") + result = self.copy() + for ind, val in other._comp.iteritems(): + result[[ind]] += val + return result + else: + return CompWithSym.__add__(self, other) + + +#****************************************************************************** + +class KroneckerDelta(CompFullySym): + r""" + Kronecker delta `\delta_{ij}`. + + INPUT: + + - ``ring`` -- commutative ring in which each component takes its value + - ``frame`` -- frame with respect to which the components are defined; + whatever type ``frame`` is, it should have some method ``__len__()`` + implemented, so that ``len(frame)`` returns the dimension, i.e. the size + of a single index range + - ``start_index`` -- (default: 0) first value of a single index; + accordingly a component index i must obey + ``start_index <= i <= start_index + dim - 1``, where ``dim = len(frame)``. + - ``output_formatter`` -- (default: ``None``) function or unbound + method called to format the output of the component access + operator ``[...]`` (method ``__getitem__``); ``output_formatter`` must + take 1 or 2 arguments: the first argument must be an instance of + ``ring`` and the second one, if any, some format specification + + EXAMPLES: + + The Kronecker delta on a 3-dimensional space:: + + sage: from sage.tensor.modules.comp import KroneckerDelta + sage: V = VectorSpace(QQ,3) + sage: d = KroneckerDelta(QQ, V.basis()) ; d + Kronecker delta of size 3x3 + sage: d[:] + [1 0 0] + [0 1 0] + [0 0 1] + + One can read, but not set, the components of a Kronecker delta:: + + sage: d[1,1] + 1 + sage: d[1,1] = 2 + Traceback (most recent call last): + ... + TypeError: the components of a Kronecker delta cannot be changed + + Examples of use with output formatters:: + + sage: d = KroneckerDelta(QQ, V.basis(), output_formatter=Rational.numerical_approx) + sage: d[:] # default format (53 bits of precision) + [ 1.00000000000000 0.000000000000000 0.000000000000000] + [0.000000000000000 1.00000000000000 0.000000000000000] + [0.000000000000000 0.000000000000000 1.00000000000000] + sage: d[:,10] # format = 10 bits of precision + [ 1.0 0.00 0.00] + [0.00 1.0 0.00] + [0.00 0.00 1.0] + sage: d = KroneckerDelta(QQ, V.basis(), output_formatter=str) + sage: d[:] + [['1', '0', '0'], ['0', '1', '0'], ['0', '0', '1']] + + """ + def __init__(self, ring, frame, start_index=0, output_formatter=None): + r""" + TEST:: + + sage: from sage.tensor.modules.comp import KroneckerDelta + sage: d = KroneckerDelta(ZZ, (1,2,3)) + sage: TestSuite(d).run() + + """ + CompFullySym.__init__(self, ring, frame, 2, start_index, + output_formatter) + for i in range(self._sindex, self._dim + self._sindex): + self._comp[(i,i)] = self._ring(1) + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLE:: + + sage: from sage.tensor.modules.comp import KroneckerDelta + sage: KroneckerDelta(ZZ, (1,2,3)) + Kronecker delta of size 3x3 + + """ + n = str(self._dim) + return "Kronecker delta of size " + n + "x" + n + + def __setitem__(self, args, value): + r""" + Should not be used (the components of a Kronecker delta are constant) + + EXAMPLE:: + + sage: from sage.tensor.modules.comp import KroneckerDelta + sage: d = KroneckerDelta(ZZ, (1,2,3)) + sage: d.__setitem__((0,0), 1) + Traceback (most recent call last): + ... + TypeError: the components of a Kronecker delta cannot be changed + + """ + raise TypeError("the components of a Kronecker delta cannot be changed") diff --git a/src/sage/tensor/modules/ext_pow_free_module.py b/src/sage/tensor/modules/ext_pow_free_module.py new file mode 100644 index 00000000000..bac021cd0d9 --- /dev/null +++ b/src/sage/tensor/modules/ext_pow_free_module.py @@ -0,0 +1,487 @@ +r""" +Exterior powers of dual free modules + +Given a free module `M` of finite rank over a commutative ring `R` +and a positive integer `p`, the *p-th exterior power* of the dual of `M` is the +set `\Lambda^p(M^*)` of all alternating forms of degree `p` on `M`, i.e. of +all multilinear maps + +.. MATH:: + + \underbrace{M\times\cdots\times M}_{p\ \; \mbox{times}} + \longrightarrow R + +that vanish whenever any of two of their arguments are equal. +Note that `\Lambda^1(M^*) = M^*` (the dual of `M`). + +`\Lambda^p(M^*)` is a free module of rank `\left({n\atop p}\right)` over `R`, +where `n` is the rank of `M`. +Accordingly, exterior powers of free modules are implemented by a class, +:class:`ExtPowerFreeModule`, which inherits from the class +:class:`~sage.tensor.modules.finite_rank_free_module.FiniteRankFreeModule`. + +AUTHORS: + +- Eric Gourgoulhon (2015): initial version + +REFERENCES: + +- K. Conrad: *Exterior powers*, + `http://www.math.uconn.edu/~kconrad/blurbs/ `_ +- Chap. 19 of S. Lang: *Algebra*, 3rd ed., Springer (New York) (2002) + +""" +#****************************************************************************** +# Copyright (C) 2015 Eric Gourgoulhon +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# http://www.gnu.org/licenses/ +#****************************************************************************** + +from sage.tensor.modules.finite_rank_free_module import FiniteRankFreeModule +from sage.tensor.modules.free_module_tensor import FreeModuleTensor +from sage.tensor.modules.free_module_alt_form import FreeModuleAltForm + +class ExtPowerFreeModule(FiniteRankFreeModule): + r""" + Class for the exterior powers of the dual of a free module of finite rank + over a commutative ring. + + Given a free module `M` of finite rank over a commutative ring `R` + and a positive integer `p`, the *p-th exterior power* of the dual of `M` is + the set `\Lambda^p(M^*)` of all alternating forms of degree `p` on `M`, + i.e. of all multilinear maps + + .. MATH:: + + \underbrace{M\times\cdots\times M}_{p\ \; \mbox{times}} + \longrightarrow R + + that vanish whenever any of two of their arguments are equal. + Note that `\Lambda^1(M^*) = M^*` (the dual of `M`). + + `\Lambda^p(M^*)` is a free module of rank `\left({n\atop p}\right)` over + `R`, where `n` is the rank of `M`. + Accordingly, the class :class:`ExtPowerFreeModule` inherits from the class + :class:`~sage.tensor.modules.finite_rank_free_module.FiniteRankFreeModule`. + + This is a Sage *parent* class, whose *element* class is + :class:`~sage.tensor.modules.free_module_alt_form.FreeModuleAltForm`. + + INPUT: + + - ``fmodule`` -- free module `M` of finite rank, as an instance of + :class:`~sage.tensor.modules.finite_rank_free_module.FiniteRankFreeModule` + - ``degree`` -- positive integer; the degree `p` of the alternating forms + - ``name`` -- (default: ``None``) string; name given to `\Lambda^p(M^*)` + - ``latex_name`` -- (default: ``None``) string; LaTeX symbol to denote + `\Lambda^p(M^*)` + + EXAMPLES: + + 2nd exterior power of the dual of a free `\ZZ`-module of rank 3:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: from sage.tensor.modules.ext_pow_free_module import ExtPowerFreeModule + sage: A = ExtPowerFreeModule(M, 2) ; A + 2nd exterior power of the dual of the Rank-3 free module M over the + Integer Ring + + Instead of importing ExtPowerFreeModule in the global name space, it is + recommended to use the module's method + :meth:`~sage.tensor.modules.finite_rank_free_module.FiniteRankFreeModule.dual_exterior_power`:: + + sage: A = M.dual_exterior_power(2) ; A + 2nd exterior power of the dual of the Rank-3 free module M over the + Integer Ring + sage: latex(A) + \Lambda^{2}\left(M^*\right) + + ``A`` is a module (actually a free module) over `\ZZ`:: + + sage: A.category() + Category of modules over Integer Ring + sage: A in Modules(ZZ) + True + sage: A.rank() + 3 + sage: A.base_ring() + Integer Ring + sage: A.base_module() + Rank-3 free module M over the Integer Ring + + ``A`` is a *parent* object, whose elements are alternating forms, + represented by instances of the class + :class:`~sage.tensor.modules.free_module_alt_form.FreeModuleAltForm`:: + + sage: a = A.an_element() ; a + Alternating form of degree 2 on the Rank-3 free module M over the + Integer Ring + sage: a.display() # expansion with respect to M's default basis (e) + e^0/\e^1 + sage: from sage.tensor.modules.free_module_alt_form import FreeModuleAltForm + sage: isinstance(a, FreeModuleAltForm) + True + sage: a in A + True + sage: A.is_parent_of(a) + True + + Elements can be constructed from ``A``. In particular, 0 yields + the zero element of ``A``:: + + sage: A(0) + Alternating form zero of degree 2 on the Rank-3 free module M over the + Integer Ring + sage: A(0) is A.zero() + True + + while non-zero elements are constructed by providing their components in a + given basis:: + + sage: e + Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer Ring + sage: comp = [[0,3,-1],[-3,0,4],[1,-4,0]] + sage: a = A(comp, basis=e, name='a') ; a + Alternating form a of degree 2 on the Rank-3 free module M over the + Integer Ring + sage: a.display(e) + a = 3 e^0/\e^1 - e^0/\e^2 + 4 e^1/\e^2 + + An alternative is to construct the alternating form from an empty list of + components and to set the nonzero components afterwards:: + + sage: a = A([], name='a') + sage: a.set_comp(e)[0,1] = 3 + sage: a.set_comp(e)[0,2] = -1 + sage: a.set_comp(e)[1,2] = 4 + sage: a.display(e) + a = 3 e^0/\e^1 - e^0/\e^2 + 4 e^1/\e^2 + + The exterior powers are unique:: + + sage: A is M.dual_exterior_power(2) + True + + The exterior power `\Lambda^1(M^*)` is nothing but `M^*`:: + + sage: M.dual_exterior_power(1) is M.dual() + True + sage: M.dual() + Dual of the Rank-3 free module M over the Integer Ring + sage: latex(M.dual()) + M^* + + Since any tensor of type (0,1) is a linear form, there is a coercion map + from the set `T^{(0,1)}(M)` of such tensors to `M^*`:: + + sage: T01 = M.tensor_module(0,1) ; T01 + Free module of type-(0,1) tensors on the Rank-3 free module M over the + Integer Ring + sage: M.dual().has_coerce_map_from(T01) + True + + There is also a coercion map in the reverse direction:: + + sage: T01.has_coerce_map_from(M.dual()) + True + + For a degree `p\geq 2`, the coercion holds only in the direction + `\Lambda^p(M^*)\rightarrow T^{(0,p)}(M)`:: + + sage: T02 = M.tensor_module(0,2) ; T02 + Free module of type-(0,2) tensors on the Rank-3 free module M over the + Integer Ring + sage: T02.has_coerce_map_from(A) + True + sage: A.has_coerce_map_from(T02) + False + + The coercion map `T^{(0,1)}(M) \rightarrow M^*` in action:: + + sage: b = T01([-2,1,4], basis=e, name='b') ; b + Type-(0,1) tensor b on the Rank-3 free module M over the Integer Ring + sage: b.display(e) + b = -2 e^0 + e^1 + 4 e^2 + sage: lb = M.dual()(b) ; lb + Linear form b on the Rank-3 free module M over the Integer Ring + sage: lb.display(e) + b = -2 e^0 + e^1 + 4 e^2 + + The coercion map `M^* \rightarrow T^{(0,1)}(M)` in action:: + + sage: tlb = T01(lb) ; tlb + Type-(0,1) tensor b on the Rank-3 free module M over the Integer Ring + sage: tlb == b + True + + The coercion map `\Lambda^2(M^*)\rightarrow T^{(0,2)}(M)` in action:: + + sage: ta = T02(a) ; ta + Type-(0,2) tensor a on the Rank-3 free module M over the Integer Ring + sage: ta.display(e) + a = 3 e^0*e^1 - e^0*e^2 - 3 e^1*e^0 + 4 e^1*e^2 + e^2*e^0 - 4 e^2*e^1 + sage: a.display(e) + a = 3 e^0/\e^1 - e^0/\e^2 + 4 e^1/\e^2 + sage: ta.symmetries() # the antisymmetry is of course preserved + no symmetry; antisymmetry: (0, 1) + + """ + + Element = FreeModuleAltForm + + def __init__(self, fmodule, degree, name=None, latex_name=None): + r""" + TEST:: + + sage: from sage.tensor.modules.ext_pow_free_module import ExtPowerFreeModule + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: A = ExtPowerFreeModule(M, 2) ; A + 2nd exterior power of the dual of the Rank-3 free module M over + the Integer Ring + sage: TestSuite(A).run() + + """ + from sage.functions.other import binomial + self._fmodule = fmodule + self._degree = degree + rank = binomial(fmodule._rank, degree) + self._zero_element = 0 # provisory (to avoid infinite recursion in what + # follows) + if degree == 1: # case of the dual + if name is None and fmodule._name is not None: + name = fmodule._name + '*' + if latex_name is None and fmodule._latex_name is not None: + latex_name = fmodule._latex_name + r'^*' + else: + if name is None and fmodule._name is not None: + name = '/\^{}('.format(degree) + fmodule._name + '*)' + if latex_name is None and fmodule._latex_name is not None: + latex_name = r'\Lambda^{' + str(degree) + r'}\left(' + \ + fmodule._latex_name + r'^*\right)' + FiniteRankFreeModule.__init__(self, fmodule._ring, rank, name=name, + latex_name=latex_name, + start_index=fmodule._sindex, + output_formatter=fmodule._output_formatter) + # Unique representation: + if self._degree in self._fmodule._dual_exterior_powers: + raise ValueError("the {}th exterior power of ".format(degree) + + "the dual of {}".format(self._fmodule) + + " has already been created") + else: + self._fmodule._dual_exterior_powers[self._degree] = self + # Zero element + self._zero_element = self._element_constructor_(name='zero', + latex_name='0') + for basis in self._fmodule._known_bases: + self._zero_element._components[basis] = \ + self._zero_element._new_comp(basis) + # (since new components are initialized to zero) + + #### Parent methods + + def _element_constructor_(self, comp=[], basis=None, name=None, + latex_name=None): + r""" + Construct an alternating form. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: A = M.dual_exterior_power(1) + sage: a = A._element_constructor_(0) ; a + Linear form zero on the Rank-3 free module M over the Integer Ring + sage: a = A._element_constructor_([2,0,-1], name='a') ; a + Linear form a on the Rank-3 free module M over the Integer Ring + sage: a.display() + a = 2 e^0 - e^2 + sage: A = M.dual_exterior_power(2) + sage: a = A._element_constructor_(0) ; a + Alternating form zero of degree 2 on the Rank-3 free module M over + the Integer Ring + sage: a = A._element_constructor_([], name='a') ; a + Alternating form a of degree 2 on the Rank-3 free module M over + the Integer Ring + sage: a[e,0,2], a[e,1,2] = 3, -1 + sage: a.display() + a = 3 e^0/\e^2 - e^1/\e^2 + + """ + if comp == 0: + return self._zero_element + if isinstance(comp, FreeModuleTensor): + # coercion of a tensor of type (0,1) to a linear form + tensor = comp # for readability + if tensor.tensor_type() == (0,1) and self._degree == 1 and \ + tensor.base_module() is self._fmodule: + resu = self.element_class(self._fmodule, 1, name=tensor._name, + latex_name=tensor._latex_name) + for basis, comp in tensor._components.iteritems(): + resu._components[basis] = comp.copy() + return resu + else: + raise TypeError("cannot coerce the {} ".format(tensor) + + "to an element of {}".format(self)) + # standard construction + resu = self.element_class(self._fmodule, self._degree, name=name, + latex_name=latex_name) + if comp: + resu.set_comp(basis)[:] = comp + return resu + + def _an_element_(self): + r""" + Construct some (unamed) alternating form. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(QQ, 4, name='M') + sage: e = M.basis('e') + sage: a = M.dual_exterior_power(1)._an_element_() ; a + Linear form on the 4-dimensional vector space M over the Rational + Field + sage: a.display() + 1/2 e^0 + sage: a = M.dual_exterior_power(2)._an_element_() ; a + Alternating form of degree 2 on the 4-dimensional vector space M + over the Rational Field + sage: a.display() + 1/2 e^0/\e^1 + sage: a = M.dual_exterior_power(3)._an_element_() ; a + Alternating form of degree 3 on the 4-dimensional vector space M + over the Rational Field + sage: a.display() + 1/2 e^0/\e^1/\e^2 + sage: a = M.dual_exterior_power(4)._an_element_() ; a + Alternating form of degree 4 on the 4-dimensional vector space M + over the Rational Field + sage: a.display() + 1/2 e^0/\e^1/\e^2/\e^3 + + """ + resu = self.element_class(self._fmodule, self._degree) + if self._fmodule._def_basis is not None: + sindex = self._fmodule._sindex + ind = [sindex + i for i in range(resu._tensor_rank)] + resu.set_comp()[ind] = self._fmodule._ring.an_element() + return resu + + def _coerce_map_from_(self, other): + r""" + Determine whether coercion to ``self`` exists from other parent. + + EXAMPLES: + + Sets of type-(0,1) tensors coerce to ``self`` if the degree is 1:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: L1 = M.dual_exterior_power(1) ; L1 + Dual of the Rank-3 free module M over the Integer Ring + sage: T01 = M.tensor_module(0,1) ; T01 + Free module of type-(0,1) tensors on the Rank-3 free module M over + the Integer Ring + sage: L1._coerce_map_from_(T01) + True + + Of course, coercions from other tensor types are meaningless:: + + sage: L1._coerce_map_from_(M.tensor_module(1,0)) + False + sage: L1._coerce_map_from_(M.tensor_module(0,2)) + False + + If the degree is larger than 1, there is no coercion:: + + sage: L2 = M.dual_exterior_power(2) ; L2 + 2nd exterior power of the dual of the Rank-3 free module M over + the Integer Ring + sage: L2._coerce_map_from_(M.tensor_module(0,2)) + False + + """ + from sage.tensor.modules.tensor_free_module import TensorFreeModule + if isinstance(other, TensorFreeModule): + # coercion of a type-(0,1) tensor to a linear form + if self._fmodule is other._fmodule and self._degree == 1 and \ + other.tensor_type() == (0,1): + return True + return False + + #### End of parent methods + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 5, name='M') + sage: M.dual_exterior_power(1)._repr_() + 'Dual of the Rank-5 free module M over the Integer Ring' + sage: M.dual_exterior_power(2)._repr_() + '2nd exterior power of the dual of the Rank-5 free module M over the Integer Ring' + sage: M.dual_exterior_power(3)._repr_() + '3rd exterior power of the dual of the Rank-5 free module M over the Integer Ring' + sage: M.dual_exterior_power(4)._repr_() + '4th exterior power of the dual of the Rank-5 free module M over the Integer Ring' + sage: M.dual_exterior_power(5)._repr_() + '5th exterior power of the dual of the Rank-5 free module M over the Integer Ring' + + """ + if self._degree == 1: + return "Dual of the {}".format(self._fmodule) + description = "{}".format(self._degree) + if self._degree == 2: + description += "nd" + elif self._degree == 3: + description += "rd" + else: + description += "th" + description += " exterior power of the dual of the {}".format( + self._fmodule) + return description + + def base_module(self): + r""" + Return the free module on which ``self`` is constructed. + + OUTPUT: + + - instance of :class:`FiniteRankFreeModule` representing the free + module on which the exterior power is defined. + + EXAMPLE:: + + sage: M = FiniteRankFreeModule(ZZ, 5, name='M') + sage: A = M.dual_exterior_power(2) + sage: A.base_module() + Rank-5 free module M over the Integer Ring + sage: A.base_module() is M + True + + """ + return self._fmodule + + def degree(self): + r""" + Return the degree of ``self``. + + OUTPUT: + + - integer `p` such that ``self`` is the exterior power `\Lambda^p(M^*)` + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 5, name='M') + sage: A = M.dual_exterior_power(2) + sage: A.degree() + 2 + sage: M.dual_exterior_power(4).degree() + 4 + + """ + return self._degree diff --git a/src/sage/tensor/modules/finite_rank_free_module.py b/src/sage/tensor/modules/finite_rank_free_module.py new file mode 100644 index 00000000000..c3735a04a9f --- /dev/null +++ b/src/sage/tensor/modules/finite_rank_free_module.py @@ -0,0 +1,2461 @@ +r""" +Free modules of finite rank + +The class :class:`FiniteRankFreeModule` implements free modules of finite rank +over a commutative ring. + +A *free module of finite rank* over a commutative ring `R` is a module `M` over +`R` that admits a *finite basis*, i.e. a finite familly of linearly independent +generators. Since `R` is commutative, it has the invariant basis number +property, so that the rank of the free module `M` is defined uniquely, as the +cardinality of any basis of `M`. + +No distinguished basis of `M` is assumed. On the contrary, many bases can be +introduced on the free module along with change-of-basis rules (as module +automorphisms). Each +module element has then various representations over the various bases. + +.. NOTE:: + + The class :class:`FiniteRankFreeModule` does not inherit from + class :class:`~sage.modules.free_module.FreeModule_generic` + nor from class + :class:`~sage.combinat.free_module.CombinatorialFreeModule`, since + both classes deal with modules with a *distinguished basis* (see + details :ref:`below `). Accordingly, the class + :class:`FiniteRankFreeModule` inherits directly from the generic class + :class:`~sage.structure.parent.Parent` with the category set to + :class:`~sage.categories.modules.Modules` (and not to + :class:`~sage.categories.modules_with_basis.ModulesWithBasis`). + +.. TODO:: + + - implement submodules + - create a FreeModules category (cf. the *TODO* statement in the + documentation of :class:`~sage.categories.modules.Modules`: *Implement + a ``FreeModules(R)`` category, when so prompted by a concrete use case*) + +AUTHORS: + +- Eric Gourgoulhon, Michal Bejger (2014-2015): initial version + +REFERENCES: + +- Chap. 10 of R. Godement : *Algebra*, Hermann (Paris) / Houghton Mifflin + (Boston) (1968) +- Chap. 3 of S. Lang : *Algebra*, 3rd ed., Springer (New York) (2002) + +EXAMPLES: + +Let us define a free module of rank 2 over `\ZZ`:: + + sage: M = FiniteRankFreeModule(ZZ, 2, name='M') ; M + Rank-2 free module M over the Integer Ring + sage: M.category() + Category of modules over Integer Ring + +We introduce a first basis on ``M``:: + + sage: e = M.basis('e') ; e + Basis (e_0,e_1) on the Rank-2 free module M over the Integer Ring + +The elements of the basis are of course module elements:: + + sage: e[0] + Element e_0 of the Rank-2 free module M over the Integer Ring + sage: e[1] + Element e_1 of the Rank-2 free module M over the Integer Ring + sage: e[0].parent() + Rank-2 free module M over the Integer Ring + +We define a module element by its components w.r.t. basis ``e``:: + + sage: u = M([2,-3], basis=e, name='u') + sage: u.display(e) + u = 2 e_0 - 3 e_1 + +Module elements can be also be created by arithmetic expressions:: + + sage: v = -2*u + 4*e[0] ; v + Element of the Rank-2 free module M over the Integer Ring + sage: v.display(e) + 6 e_1 + sage: u == 2*e[0] - 3*e[1] + True + +We define a second basis on ``M`` from a family of linearly independent +elements:: + + sage: f = M.basis('f', from_family=(e[0]-e[1], -2*e[0]+3*e[1])) ; f + Basis (f_0,f_1) on the Rank-2 free module M over the Integer Ring + sage: f[0].display(e) + f_0 = e_0 - e_1 + sage: f[1].display(e) + f_1 = -2 e_0 + 3 e_1 + +We may of course express the elements of basis ``e`` in terms of basis ``f``:: + + sage: e[0].display(f) + e_0 = 3 f_0 + f_1 + sage: e[1].display(f) + e_1 = 2 f_0 + f_1 + +as well as any module element:: + + sage: u.display(f) + u = -f_1 + sage: v.display(f) + 12 f_0 + 6 f_1 + +The two bases are related by a module automorphism:: + + sage: a = M.change_of_basis(e,f) ; a + Automorphism of the Rank-2 free module M over the Integer Ring + sage: a.parent() + General linear group of the Rank-2 free module M over the Integer Ring + sage: a.matrix(e) + [ 1 -2] + [-1 3] + +Let us check that basis ``f`` is indeed the image of basis ``e`` by ``a``:: + + sage: f[0] == a(e[0]) + True + sage: f[1] == a(e[1]) + True + +The reverse change of basis is of course the inverse automorphism:: + + sage: M.change_of_basis(f,e) == a^(-1) + True + +We introduce a new module element via its components w.r.t. basis ``f``:: + + sage: v = M([2,4], basis=f, name='v') + sage: v.display(f) + v = 2 f_0 + 4 f_1 + +The sum of the two module elements ``u`` and ``v`` can be performed even if +they have been defined on different bases, thanks to the known relation +between the two bases:: + + sage: s = u + v ; s + Element u+v of the Rank-2 free module M over the Integer Ring + +We can display the result in either basis:: + + sage: s.display(e) + u+v = -4 e_0 + 7 e_1 + sage: s.display(f) + u+v = 2 f_0 + 3 f_1 + +Tensor products of elements are implemented:: + + sage: t = u*v ; t + Type-(2,0) tensor u*v on the Rank-2 free module M over the Integer Ring + sage: t.parent() + Free module of type-(2,0) tensors on the + Rank-2 free module M over the Integer Ring + sage: t.display(e) + u*v = -12 e_0*e_0 + 20 e_0*e_1 + 18 e_1*e_0 - 30 e_1*e_1 + sage: t.display(f) + u*v = -2 f_1*f_0 - 4 f_1*f_1 + +We can access to tensor components w.r.t. to a given basis via the square +bracket operator:: + + sage: t[e,0,1] + 20 + sage: t[f,1,0] + -2 + sage: u[e,0] + 2 + sage: u[e,:] + [2, -3] + sage: u[f,:] + [0, -1] + +The parent of the automorphism ``a`` is the group `\mathrm{GL}(M)`, but +``a`` can also be considered as a tensor of type `(1,1)` on ``M``:: + + sage: a.parent() + General linear group of the Rank-2 free module M over the Integer Ring + sage: a.tensor_type() + (1, 1) + sage: a.display(e) + e_0*e^0 - 2 e_0*e^1 - e_1*e^0 + 3 e_1*e^1 + sage: a.display(f) + f_0*f^0 - 2 f_0*f^1 - f_1*f^0 + 3 f_1*f^1 + +As such, we can form its tensor product with ``t``, yielding a tensor of +type `(3,1)`:: + + sage: t*a + Type-(3,1) tensor on the Rank-2 free module M over the Integer Ring + sage: (t*a).display(e) + -12 e_0*e_0*e_0*e^0 + 24 e_0*e_0*e_0*e^1 + 12 e_0*e_0*e_1*e^0 + - 36 e_0*e_0*e_1*e^1 + 20 e_0*e_1*e_0*e^0 - 40 e_0*e_1*e_0*e^1 + - 20 e_0*e_1*e_1*e^0 + 60 e_0*e_1*e_1*e^1 + 18 e_1*e_0*e_0*e^0 + - 36 e_1*e_0*e_0*e^1 - 18 e_1*e_0*e_1*e^0 + 54 e_1*e_0*e_1*e^1 + - 30 e_1*e_1*e_0*e^0 + 60 e_1*e_1*e_0*e^1 + 30 e_1*e_1*e_1*e^0 + - 90 e_1*e_1*e_1*e^1 + +The parent of `t\otimes a` is itself a free module of finite rank over `\ZZ`:: + + sage: T = (t*a).parent() ; T + Free module of type-(3,1) tensors on the Rank-2 free module M over the + Integer Ring + sage: T.base_ring() + Integer Ring + sage: T.rank() + 16 + +.. _diff-FreeModule: + +.. RUBRIC:: Differences between ``FiniteRankFreeModule`` and ``FreeModule`` + (or ``VectorSpace``) + +To illustrate the differences, let us create two free modules of rank 3 over +`\ZZ`, one with ``FiniteRankFreeModule`` and the other one with +``FreeModule``:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') ; M + Rank-3 free module M over the Integer Ring + sage: N = FreeModule(ZZ, 3) ; N + Ambient free module of rank 3 over the principal ideal domain Integer Ring + +The main difference is that ``FreeModule`` returns a free module with a +distinguished basis, while ``FiniteRankFreeModule`` does not:: + + sage: N.basis() + [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + sage: M.bases() + [] + sage: M.print_bases() + No basis has been defined on the Rank-3 free module M over the Integer Ring + +This is also revealed by the category of each module:: + + sage: M.category() + Category of modules over Integer Ring + sage: N.category() + Category of modules with basis over Integer Ring + +In other words, the module created by ``FreeModule`` is actually `\ZZ^3`, +while, in the absence of any distinguished basis, no *canonical* isomorphism +relates the module created by ``FiniteRankFreeModule`` to `\ZZ^3`:: + + sage: N is ZZ^3 + True + sage: M is ZZ^3 + False + sage: M == ZZ^3 + False + +Because it is `\ZZ^3`, ``N`` is unique, while there may be various modules +of the same rank over the same ring created by ``FiniteRankFreeModule``; +they are then distinguished by their names (actually by the complete +sequence of arguments of ``FiniteRankFreeModule``):: + + sage: N1 = FreeModule(ZZ, 3) ; N1 + Ambient free module of rank 3 over the principal ideal domain Integer Ring + sage: N1 is N # FreeModule(ZZ, 3) is unique + True + sage: M1 = FiniteRankFreeModule(ZZ, 3, name='M_1') ; M1 + Rank-3 free module M_1 over the Integer Ring + sage: M1 is M # M1 and M are different rank-3 modules over ZZ + False + sage: M1b = FiniteRankFreeModule(ZZ, 3, name='M_1') ; M1b + Rank-3 free module M_1 over the Integer Ring + sage: M1b is M1 # because M1b and M1 have the same name + True + +As illustrated above, various bases can be introduced on the module created by +``FiniteRankFreeModule``:: + + sage: e = M.basis('e') ; e + Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer Ring + sage: f = M.basis('f', from_family=(-e[0], e[1]-e[2], -2*e[1]+3*e[2])) ; f + Basis (f_0,f_1,f_2) on the Rank-3 free module M over the Integer Ring + sage: M.bases() + [Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer Ring, + Basis (f_0,f_1,f_2) on the Rank-3 free module M over the Integer Ring] + +Each element of a basis is accessible via its index:: + + sage: e[0] + Element e_0 of the Rank-3 free module M over the Integer Ring + sage: e[0].parent() + Rank-3 free module M over the Integer Ring + sage: f[1] + Element f_1 of the Rank-3 free module M over the Integer Ring + sage: f[1].parent() + Rank-3 free module M over the Integer Ring + +while on module ``N``, the element of the (unique) basis is accessible +directly from the module symbol:: + + sage: N.0 + (1, 0, 0) + sage: N.1 + (0, 1, 0) + sage: N.0.parent() + Ambient free module of rank 3 over the principal ideal domain Integer Ring + +The arithmetic of elements is similar; the difference lies in the display: +a basis has to be specified for elements of ``M``, while elements of ``N`` are +displayed directly as elements of `\ZZ^3`:: + + sage: u = 2*e[0] - 3*e[2] ; u + Element of the Rank-3 free module M over the Integer Ring + sage: u.display(e) + 2 e_0 - 3 e_2 + sage: u.display(f) + -2 f_0 - 6 f_1 - 3 f_2 + sage: u[e,:] + [2, 0, -3] + sage: u[f,:] + [-2, -6, -3] + sage: v = 2*N.0 - 3*N.2 ; v + (2, 0, -3) + +For the case of ``M``, in order to avoid to specify the basis if the user is +always working with the same basis (e.g. only one basis has been defined), +the concept of *default basis* has been introduced:: + + sage: M.default_basis() + Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer Ring + sage: M.print_bases() + Bases defined on the Rank-3 free module M over the Integer Ring: + - (e_0,e_1,e_2) (default basis) + - (f_0,f_1,f_2) + +This is different from the *distinguished basis* of ``N``: it simply means that +the mention of the basis can be omitted in function arguments:: + + sage: u.display() # equivalent to u.display(e) + 2 e_0 - 3 e_2 + sage: u[:] # equivalent to u[e,:] + [2, 0, -3] + +At any time, the default basis can be changed:: + + sage: M.set_default_basis(f) + sage: u.display() + -2 f_0 - 6 f_1 - 3 f_2 + +Another difference between ``FiniteRankFreeModule`` and ``FreeModule`` is that +for the former the range of indices can be specified (by default, it starts +from 0):: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M', start_index=1) ; M + Rank-3 free module M over the Integer Ring + sage: e = M.basis('e') ; e # compare with (e_0,e_1,e_2) above + Basis (e_1,e_2,e_3) on the Rank-3 free module M over the Integer Ring + sage: e[1], e[2], e[3] + (Element e_1 of the Rank-3 free module M over the Integer Ring, + Element e_2 of the Rank-3 free module M over the Integer Ring, + Element e_3 of the Rank-3 free module M over the Integer Ring) + +All the above holds for ``VectorSpace`` instead of ``FreeModule``: the object +created by ``VectorSpace`` is actually a Cartesian power of the base field:: + + sage: V = VectorSpace(QQ,3) ; V + Vector space of dimension 3 over Rational Field + sage: V.category() + Category of vector spaces over Rational Field + sage: V is QQ^3 + True + sage: V.basis() + [ + (1, 0, 0), + (0, 1, 0), + (0, 0, 1) + ] + +To create a vector space without any distinguished basis, one has to use +``FiniteRankFreeModule``:: + + sage: V = FiniteRankFreeModule(QQ, 3, name='V') ; V + 3-dimensional vector space V over the Rational Field + sage: V.category() + Category of vector spaces over Rational Field + sage: V.bases() + [] + sage: V.print_bases() + No basis has been defined on the 3-dimensional vector space V over the + Rational Field + +The class :class:`FiniteRankFreeModule` has been created for the needs +of the `SageManifolds project `_, where +free modules do not have any distinguished basis. Too kinds of free modules +occur in the context of differentiable manifolds (see +`here `_ for more +details): + +- the tangent vector space at any point of the manifold +- the set of vector fields on a parallelizable open subset `U` of the manifold, + which is a free module over the algebra of scalar fields on `U`. + +For instance, without any specific coordinate choice, no basis can be +distinguished in a tangent space. + +On the other side, the modules created by ``FreeModule`` have much more +algebraic functionalities than those created by ``FiniteRankFreeModule``. In +particular, submodules have not been implemented yet in +:class:`FiniteRankFreeModule`. Moreover, modules resulting from ``FreeModule`` +are tailored to the specific kind of their base ring: + +- free module over a commutative ring that is not an integral domain + (`\ZZ/6\ZZ`):: + + sage: R = IntegerModRing(6) ; R + Ring of integers modulo 6 + sage: FreeModule(R, 3) + Ambient free module of rank 3 over Ring of integers modulo 6 + sage: type(FreeModule(R, 3)) + + +- free module over an integral domain that is not principal (`\ZZ[X]`):: + + sage: R. = ZZ[] ; R + Univariate Polynomial Ring in X over Integer Ring + sage: FreeModule(R, 3) + Ambient free module of rank 3 over the integral domain Univariate + Polynomial Ring in X over Integer Ring + sage: type(FreeModule(R, 3)) + + +- free module over a principal ideal domain (`\ZZ`):: + + sage: R = ZZ ; R + Integer Ring + sage: FreeModule(R,3) + Ambient free module of rank 3 over the principal ideal domain Integer Ring + sage: type(FreeModule(R, 3)) + + +On the contrary, all objects constructed with ``FiniteRankFreeModule`` belong +to the same class:: + + sage: R = IntegerModRing(6) + sage: type(FiniteRankFreeModule(R, 3)) + + sage: R. = ZZ[] + sage: type(FiniteRankFreeModule(R, 3)) + + sage: R = ZZ + sage: type(FiniteRankFreeModule(R, 3)) + + + +.. RUBRIC:: Differences between ``FiniteRankFreeModule`` and + ``CombinatorialFreeModule`` + +An alternative to construct free modules in Sage is +:class:`~sage.combinat.free_module.CombinatorialFreeModule`. +However, as ``FreeModule``, it leads to a module with a distinguished basis:: + + sage: N = CombinatorialFreeModule(ZZ, [1,2,3]) ; N + Free module generated by {1, 2, 3} over Integer Ring + sage: N.category() + Category of modules with basis over Integer Ring + +The distinguished basis is returned by the method ``basis()``:: + + sage: b = N.basis() ; b + Finite family {1: B[1], 2: B[2], 3: B[3]} + sage: b[1] + B[1] + sage: b[1].parent() + Free module generated by {1, 2, 3} over Integer Ring + +For the free module ``M`` created above with ``FiniteRankFreeModule``, the +method ``basis`` has at least one argument: the symbol string that +specifies which basis is required:: + + sage: e = M.basis('e') ; e + Basis (e_1,e_2,e_3) on the Rank-3 free module M over the Integer Ring + sage: e[1] + Element e_1 of the Rank-3 free module M over the Integer Ring + sage: e[1].parent() + Rank-3 free module M over the Integer Ring + +The arithmetic of elements is similar:: + + sage: u = 2*e[1] - 5*e[3] ; u + Element of the Rank-3 free module M over the Integer Ring + sage: v = 2*b[1] - 5*b[3] ; v + 2*B[1] - 5*B[3] + +One notices that elements of ``N`` are displayed directly in terms of their +expansions on the distinguished basis. For elements of ``M``, one has to use +the method +:meth:`~sage.tensor.modules.free_module_tensor.FreeModuleTensor.display` +in order to specify the basis:: + + sage: u.display(e) + 2 e_1 - 5 e_3 + +The components on the basis are returned by the square bracket operator for +``M`` and by the method ``coefficient`` for ``N``:: + + sage: [u[e,i] for i in {1,2,3}] + [2, 0, -5] + sage: u[e,:] # a shortcut for the above + [2, 0, -5] + sage: [v.coefficient(i) for i in {1,2,3}] + [2, 0, -5] + +""" +#****************************************************************************** +# Copyright (C) 2015 Eric Gourgoulhon +# Copyright (C) 2015 Michal Bejger +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# http://www.gnu.org/licenses/ +#****************************************************************************** + +from sage.structure.unique_representation import UniqueRepresentation +from sage.structure.parent import Parent +from sage.categories.modules import Modules +from sage.categories.rings import Rings +from sage.categories.fields import Fields +from sage.tensor.modules.free_module_tensor import FiniteRankFreeModuleElement + +class FiniteRankFreeModule(UniqueRepresentation, Parent): + r""" + Free module of finite rank over a commutative ring. + + A *free module of finite rank* over a commutative ring `R` is a module `M` + over `R` that admits a *finite basis*, i.e. a finite familly of linearly + independent generators. Since `R` is commutative, it has the invariant + basis number property, so that the rank of the free module `M` is defined + uniquely, as the cardinality of any basis of `M`. + + No distinguished basis of `M` is assumed. On the contrary, many bases can be + introduced on the free module along with change-of-basis rules (as module + automorphisms). Each + module element has then various representations over the various bases. + + .. NOTE:: + + The class :class:`FiniteRankFreeModule` does not inherit from + class :class:`~sage.modules.free_module.FreeModule_generic` + nor from class + :class:`~sage.combinat.free_module.CombinatorialFreeModule`, since + both classes deal with modules with a *distinguished basis* (see + details :ref:`above `). + Moreover, following the recommendation exposed in trac ticket + `#16427 `_ + the class :class:`FiniteRankFreeModule` inherits directly from + :class:`~sage.structure.parent.Parent` (with the category set to + :class:`~sage.categories.modules.Modules`) and not from the Cython + class :class:`~sage.modules.module.Module`. + + The class :class:`FiniteRankFreeModule` is a Sage *parent* class, + the corresponding *element* class being + :class:`~sage.tensor.modules.free_module_tensor.FiniteRankFreeModuleElement`. + + INPUT: + + - ``ring`` -- commutative ring `R` over which the free module is + constructed + - ``rank`` -- positive integer; rank of the free module + - ``name`` -- (default: ``None``) string; name given to the free module + - ``latex_name`` -- (default: ``None``) string; LaTeX symbol to denote + the freemodule; if none is provided, it is set to ``name`` + - ``start_index`` -- (default: 0) integer; lower bound of the range of + indices in bases defined on the free module + - ``output_formatter`` -- (default: ``None``) function or unbound + method called to format the output of the tensor components; + ``output_formatter`` must take 1 or 2 arguments: the first argument + must be an element of the ring `R` and the second one, if any, some + format specification + + EXAMPLES: + + Free module of rank 3 over `\ZZ`:: + + sage: FiniteRankFreeModule._clear_cache_() # for doctests only + sage: M = FiniteRankFreeModule(ZZ, 3) ; M + Rank-3 free module over the Integer Ring + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') ; M # declaration with a name + Rank-3 free module M over the Integer Ring + sage: M.category() + Category of modules over Integer Ring + sage: M.base_ring() + Integer Ring + sage: M.rank() + 3 + + If the base ring is a field, the free module is in the category of vector + spaces:: + + sage: V = FiniteRankFreeModule(QQ, 3, name='V') ; V + 3-dimensional vector space V over the Rational Field + sage: V.category() + Category of vector spaces over Rational Field + + The LaTeX output is adjusted via the parameter ``latex_name``:: + + sage: latex(M) # the default is the symbol provided in the string ``name`` + M + sage: M = FiniteRankFreeModule(ZZ, 3, name='M', latex_name=r'\mathcal{M}') + sage: latex(M) + \mathcal{M} + + The free module M has no distinguished basis:: + + sage: M in ModulesWithBasis(ZZ) + False + sage: M in Modules(ZZ) + True + + In particular, no basis is initialized at the module construction:: + + sage: M.print_bases() + No basis has been defined on the Rank-3 free module M over the Integer Ring + sage: M.bases() + [] + + Bases have to be introduced by means of the method :meth:`basis`, + the first defined basis being considered as the *default basis*, meaning + it can be skipped in function arguments required a basis (this can + be changed by means of the method :meth:`set_default_basis`):: + + sage: e = M.basis('e') ; e + Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer Ring + sage: M.default_basis() + Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer Ring + + A second basis can be created from a family of linearly independent + elements expressed in terms of basis ``e``:: + + sage: f = M.basis('f', from_family=(-e[0], e[1]+e[2], 2*e[1]+3*e[2])) + sage: f + Basis (f_0,f_1,f_2) on the Rank-3 free module M over the Integer Ring + sage: M.print_bases() + Bases defined on the Rank-3 free module M over the Integer Ring: + - (e_0,e_1,e_2) (default basis) + - (f_0,f_1,f_2) + sage: M.bases() + [Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer Ring, + Basis (f_0,f_1,f_2) on the Rank-3 free module M over the Integer Ring] + + M is a *parent* object, whose elements are instances of + :class:`~sage.tensor.modules.free_module_tensor.FiniteRankFreeModuleElement` + (actually a dynamically generated subclass of it):: + + sage: v = M.an_element() ; v + Element of the Rank-3 free module M over the Integer Ring + sage: from sage.tensor.modules.free_module_tensor import FiniteRankFreeModuleElement + sage: isinstance(v, FiniteRankFreeModuleElement) + True + sage: v in M + True + sage: M.is_parent_of(v) + True + sage: v.display() # expansion w.r.t. the default basis (e) + e_0 + e_1 + e_2 + sage: v.display(f) + -f_0 + f_1 + + The test suite of the category of modules is passed:: + + sage: TestSuite(M).run() + + Constructing an element of ``M`` from (the integer) 0 yields + the zero element of ``M``:: + + sage: M(0) + Element zero of the Rank-3 free module M over the Integer Ring + sage: M(0) is M.zero() + True + + Non-zero elements are constructed by providing their components in + a given basis:: + + sage: v = M([-1,0,3]) ; v # components in the default basis (e) + Element of the Rank-3 free module M over the Integer Ring + sage: v.display() # expansion w.r.t. the default basis (e) + -e_0 + 3 e_2 + sage: v.display(f) + f_0 - 6 f_1 + 3 f_2 + sage: v = M([-1,0,3], basis=f) ; v # components in a specific basis + Element of the Rank-3 free module M over the Integer Ring + sage: v.display(f) + -f_0 + 3 f_2 + sage: v.display() + e_0 + 6 e_1 + 9 e_2 + sage: v = M([-1,0,3], basis=f, name='v') ; v + Element v of the Rank-3 free module M over the Integer Ring + sage: v.display(f) + v = -f_0 + 3 f_2 + sage: v.display() + v = e_0 + 6 e_1 + 9 e_2 + + An alternative is to construct the element from an empty list of + componentsand to set the nonzero components afterwards:: + + sage: v = M([], name='v') + sage: v[e,0] = -1 + sage: v[e,2] = 3 + sage: v.display(e) + v = -e_0 + 3 e_2 + + Indices on the free module, such as indices labelling the element of a + basis, are provided by the generator method :meth:`irange`. By default, + they range from 0 to the module's rank minus one:: + + sage: for i in M.irange(): print i, + 0 1 2 + + This can be changed via the parameter ``start_index`` in the module + construction:: + + sage: M1 = FiniteRankFreeModule(ZZ, 3, name='M', start_index=1) + sage: for i in M1.irange(): print i, + 1 2 3 + + The parameter ``output_formatter`` in the constructor of the free module + is used to set the output format of tensor components:: + + sage: N = FiniteRankFreeModule(QQ, 3, output_formatter=Rational.numerical_approx) + sage: e = N.basis('e') + sage: v = N([1/3, 0, -2], basis=e) + sage: v[e,:] + [0.333333333333333, 0.000000000000000, -2.00000000000000] + sage: v.display(e) # default format (53 bits of precision) + 0.333333333333333 e_0 - 2.00000000000000 e_2 + sage: v.display(e, format_spec=10) # 10 bits of precision + 0.33 e_0 - 2.0 e_2 + + """ + + Element = FiniteRankFreeModuleElement + + def __init__(self, ring, rank, name=None, latex_name=None, start_index=0, + output_formatter=None): + r""" + See :class:`FiniteRankFreeModule` for documentation and examples. + + TESTS:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: TestSuite(M).run() + sage: e = M.basis('e') + sage: TestSuite(M).run() + sage: f = M.basis('f') + sage: TestSuite(M).run() + + """ + if ring not in Rings().Commutative(): + raise TypeError("the module base ring must be commutative") + Parent.__init__(self, base=ring, category=Modules(ring)) + self._ring = ring # same as self._base + self._rank = rank + self._name = name + if latex_name is None: + self._latex_name = self._name + else: + self._latex_name = latex_name + self._sindex = start_index + self._output_formatter = output_formatter + # Dictionary of the tensor modules built on self + # (keys = (k,l) --the tensor type) : + self._tensor_modules = {(1,0): self} # self is considered as the set of + # tensors of type (1,0) + # Dictionary of exterior powers of the dual of self + # (keys = p --the power degree) : + self._dual_exterior_powers = {} + self._known_bases = [] # List of known bases on the free module + self._def_basis = None # default basis + self._basis_changes = {} # Dictionary of the changes of bases + # Zero element: + if not hasattr(self, '_zero_element'): + self._zero_element = self._element_constructor_(name='zero', + latex_name='0') + # Identity automorphism: + self._identity_map = None # to be set by self.identity_map() + # General linear group: + self._general_linear_group = None # to be set by + # self.general_linear_group() + + #### Parent methods + + def _element_constructor_(self, comp=[], basis=None, name=None, + latex_name=None): + r""" + Construct an element of ``self``. + + EXAMPLES:: + + sage: FiniteRankFreeModule._clear_cache_() # for doctests only + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: v = M._element_constructor_(comp=[1,0,-2], basis=e, name='v') ; v + Element v of the Rank-3 free module M over the Integer Ring + sage: v.display() + v = e_0 - 2 e_2 + sage: v == M([1,0,-2]) + True + sage: v = M._element_constructor_(0) ; v + Element zero of the Rank-3 free module M over the Integer Ring + sage: v = M._element_constructor_() ; v + Element of the Rank-3 free module M over the Integer Ring + + """ + if comp == 0: + return self._zero_element + resu = self.element_class(self, name=name, latex_name=latex_name) + if comp: + resu.set_comp(basis)[:] = comp + return resu + + def _an_element_(self): + r""" + Construct some (unamed) element of ``self``. + + EXAMPLE:: + + sage: FiniteRankFreeModule._clear_cache_() # for doctests only + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: v = M._an_element_(); v + Element of the Rank-3 free module M over the Integer Ring + sage: v.display() + e_0 + e_1 + e_2 + sage: v == M.an_element() + True + sage: v.parent() + Rank-3 free module M over the Integer Ring + + """ + if self._def_basis is None: + self.basis('e') + resu = self.element_class(self) + resu.set_comp()[:] = [self._ring.an_element() for i in range(self._rank)] + return resu + + #### End of parent methods + + #### Methods to be redefined by derived classes #### + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLE:: + + sage: FiniteRankFreeModule(ZZ, 3, name='M') + Rank-3 free module M over the Integer Ring + + """ + if self._ring in Fields(): + description = "{}-dimensional vector space ".format(self._rank) + else: + description = "Rank-{} free module ".format(self._rank) + if self._name is not None: + description += self._name + " " + description += "over the {}".format(self._ring) + return description + + def _Hom_(self, other, category=None): + r""" + Construct the set of homomorphisms ``self`` --> ``other``. + + INPUT: + + - ``other`` -- another free module of finite rank over the same ring + as ``self`` + - ``category`` -- (default: ``None``) not used here (to ensure + compatibility with generic hook ``_Hom_``) + + OUTPUT: + + - the hom-set Hom(M,N), where M is ``self`` and N is ``other`` + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: N = FiniteRankFreeModule(ZZ, 2, name='N') + sage: H = M._Hom_(N) ; H + Set of Morphisms from Rank-3 free module M over the Integer Ring + to Rank-2 free module N over the Integer Ring in Category of + modules over Integer Ring + sage: H = Hom(M,N) ; H # indirect doctest + Set of Morphisms from Rank-3 free module M over the Integer Ring + to Rank-2 free module N over the Integer Ring in Category of + modules over Integer Ring + + """ + from free_module_homset import FreeModuleHomset + return FreeModuleHomset(self, other) + + def tensor_module(self, k, l): + r""" + Return the free module of all tensors of type `(k, l)` defined on + ``self``. + + INPUT: + + - ``k`` -- non-negative integer; the contravariant rank, the tensor + type being `(k, l)` + - ``l`` -- non-negative integer; the covariant rank, the tensor type + being `(k, l)` + + OUTPUT: + + - instance of + :class:`~sage.tensor.modules.tensor_free_module.TensorFreeModule` + representing the free module + `T^{(k,l)}(M)` of type-`(k,l)` tensors on the free module ``self`` + + EXAMPLES: + + Tensor modules over a free module over `\ZZ`:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: T = M.tensor_module(1,2) ; T + Free module of type-(1,2) tensors on the Rank-3 free module M + over the Integer Ring + sage: T.an_element() + Type-(1,2) tensor on the Rank-3 free module M over the Integer Ring + + Tensor modules are unique:: + + sage: M.tensor_module(1,2) is T + True + + The base module is itself the module of all type-`(1,0)` tensors:: + + sage: M.tensor_module(1,0) is M + True + + See :class:`~sage.tensor.modules.tensor_free_module.TensorFreeModule` + for more documentation. + + """ + from sage.tensor.modules.tensor_free_module import TensorFreeModule + if (k,l) not in self._tensor_modules: + self._tensor_modules[(k,l)] = TensorFreeModule(self, (k,l)) + return self._tensor_modules[(k,l)] + + def dual_exterior_power(self, p): + r""" + Return the `p`-th exterior power of the dual of ``self``. + + If `M` stands for the free module ``self``, the *p-th exterior power* + of the dual of `M` is the set + `\Lambda^p(M^*)` of all *alternating forms of degree* `p` on `M`, i.e. + of all multilinear maps + + .. MATH:: + + \underbrace{M\times\cdots\times M}_{p\ \; \mbox{times}} + \longrightarrow R + + that vanish whenever any of two of their arguments are equal. + `\Lambda^p(M^*)` is a free module of rank `\left({n\atop p}\right)` + over the same ring as `M`, where `n` is the rank of `M`. + + INPUT: + + - ``p`` -- non-negative integer + + OUTPUT: + + - for `p\geq 1`, instance of + :class:`~sage.tensor.modules.ext_pow_free_module.ExtPowerFreeModule` + representing the free module `\Lambda^p(M^*)`; for `p=0`, the + base ring `R` is returned instead + + EXAMPLES: + + Exterior powers of the dual of a free `\ZZ`-module of rank 3:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: M.dual_exterior_power(0) # return the base ring + Integer Ring + sage: M.dual_exterior_power(1) # return the dual module + Dual of the Rank-3 free module M over the Integer Ring + sage: M.dual_exterior_power(1) is M.dual() + True + sage: M.dual_exterior_power(2) + 2nd exterior power of the dual of the Rank-3 free module M over the Integer Ring + sage: M.dual_exterior_power(2).an_element() + Alternating form of degree 2 on the Rank-3 free module M over the Integer Ring + sage: M.dual_exterior_power(2).an_element().display() + e^0/\e^1 + sage: M.dual_exterior_power(3) + 3rd exterior power of the dual of the Rank-3 free module M over the Integer Ring + sage: M.dual_exterior_power(3).an_element() + Alternating form of degree 3 on the Rank-3 free module M over the Integer Ring + sage: M.dual_exterior_power(3).an_element().display() + e^0/\e^1/\e^2 + + See + :class:`~sage.tensor.modules.ext_pow_free_module.ExtPowerFreeModule` + for more documentation. + + """ + from sage.tensor.modules.ext_pow_free_module import ExtPowerFreeModule + if p == 0: + return self._ring + if p not in self._dual_exterior_powers: + self._dual_exterior_powers[p] = ExtPowerFreeModule(self, p) + return self._dual_exterior_powers[p] + + def general_linear_group(self): + r""" + Return the general linear group of ``self``. + + If ``self`` is the free module `M`, the *general linear group* is the + group `\mathrm{GL}(M)` of automorphisms of `M`. + + OUTPUT: + + - instance of class + :class:`~sage.tensor.modules.free_module_linear_group.FreeModuleLinearGroup` + representing `\mathrm{GL}(M)` + + EXAMPLES: + + The general linear group of a rank-3 free module:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: GL = M.general_linear_group() ; GL + General linear group of the Rank-3 free module M over the Integer Ring + sage: GL.category() + Category of groups + sage: type(GL) + + + There is a unique instance of the general linear group:: + + sage: M.general_linear_group() is GL + True + + The group identity element:: + + sage: GL.one() + Identity map of the Rank-3 free module M over the Integer Ring + sage: GL.one().matrix(e) + [1 0 0] + [0 1 0] + [0 0 1] + + An element:: + + sage: GL.an_element() + Automorphism of the Rank-3 free module M over the Integer Ring + sage: GL.an_element().matrix(e) + [ 1 0 0] + [ 0 -1 0] + [ 0 0 1] + + See + :class:`~sage.tensor.modules.free_module_linear_group.FreeModuleLinearGroup` + for more documentation. + + """ + from sage.tensor.modules.free_module_linear_group import \ + FreeModuleLinearGroup + if self._general_linear_group is None: + self._general_linear_group = FreeModuleLinearGroup(self) + return self._general_linear_group + + def basis(self, symbol, latex_symbol=None, from_family=None): + r""" + Define or return a basis of the free module ``self``. + + Let `M` denotes the free module ``self`` and `n` its rank. + + The basis can be defined from a set of `n` linearly independent + elements of `M` by means of the argument ``from_family``. + If ``from_family`` is not specified, the basis is created from + scratch and, at this stage, is unrelated to bases that could have been + defined previously on `M`. It can be related afterwards by means of + the method :meth:`set_change_of_basis`. + + If the basis specified by the given symbol already exists, it is + simply returned, whatever the value of the arguments ``latex_symbol`` + or ``from_family``. + + Note that another way to construct a basis of ``self`` is to use + the method + :meth:`~sage.tensor.modules.free_module_basis.FreeModuleBasis.new_basis` + on an existing basis, with the automorphism relating the two bases as + an argument. + + INPUT: + + - ``symbol`` -- string; a letter (of a few letters) to denote a generic + element of the basis + - ``latex_symbol`` -- (default: ``None``) string; symbol to denote a + generic element of the basis; if ``None``, the value of ``symbol`` + is used + - ``from_family`` -- (default: ``None``) a tuple of `n` linearly + independent elements of the free module ``self`` (`n` being the + rank of ``self``) + + OUTPUT: + + - instance of + :class:`~sage.tensor.modules.free_module_basis.FreeModuleBasis` + representing a basis on ``self`` + + EXAMPLES: + + Bases on a rank-3 free module:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') ; e + Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer Ring + sage: e[0] + Element e_0 of the Rank-3 free module M over the Integer Ring + sage: latex(e) + \left(e_0,e_1,e_2\right) + + The LaTeX symbol can be set explicitely, as the second argument of + :meth:`basis`:: + + sage: eps = M.basis('eps', r'\epsilon') ; eps + Basis (eps_0,eps_1,eps_2) on the Rank-3 free module M + over the Integer Ring + sage: latex(eps) + \left(\epsilon_0,\epsilon_1,\epsilon_2\right) + + If the provided symbol is that of an already defined basis, the latter + is returned (no new basis is created):: + + sage: M.basis('e') is e + True + sage: M.basis('eps') is eps + True + + The individual elements of the basis are labelled according the + parameter ``start_index`` provided at the free module construction:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M', start_index=1) + sage: e = M.basis('e') ; e + Basis (e_1,e_2,e_3) on the Rank-3 free module M over the Integer Ring + sage: e[1] + Element e_1 of the Rank-3 free module M over the Integer Ring + + Construction of a basis from a family of linearly independent module + elements:: + + sage: f1 = -e[2] + sage: f2 = 4*e[1] + 3*e[3] + sage: f3 = 7*e[1] + 5*e[3] + sage: f = M.basis('f', from_family=(f1,f2,f3)) + sage: f[1].display() + f_1 = -e_2 + sage: f[2].display() + f_2 = 4 e_1 + 3 e_3 + sage: f[3].display() + f_3 = 7 e_1 + 5 e_3 + + The change-of-basis automorphisms have been registered:: + + sage: M.change_of_basis(e,f).matrix(e) + [ 0 4 7] + [-1 0 0] + [ 0 3 5] + sage: M.change_of_basis(f,e).matrix(e) + [ 0 -1 0] + [-5 0 7] + [ 3 0 -4] + sage: M.change_of_basis(f,e) == M.change_of_basis(e,f).inverse() + True + + Check of the change-of-basis e --> f:: + + sage: a = M.change_of_basis(e,f) ; a + Automorphism of the Rank-3 free module M over the Integer Ring + sage: all( f[i] == a(e[i]) for i in M.irange() ) + True + + For more documentation on bases see + :class:`~sage.tensor.modules.free_module_basis.FreeModuleBasis`. + + """ + from free_module_basis import FreeModuleBasis + for other in self._known_bases: + if symbol == other._symbol: + return other + resu = FreeModuleBasis(self, symbol, latex_symbol) + if from_family: + n = self._rank + if len(from_family) != n: + raise ValueError("the size of the family is not {}".format(n)) + for ff in from_family: + if ff not in self: + raise TypeError("{} is not an element of {}".format(ff, + self)) + # The automorphisms relating the family to previously defined + # bases are registered: + ff0 = from_family[0] + for basis in ff0._components: + try: + comp = [ff.components(basis) for ff in from_family] + except ValueError: + continue + mat = [[comp_ff[[i]] for comp_ff in comp] + for i in self.irange()] + aut = self.automorphism() + aut.set_comp(basis)[:] = mat + self.set_change_of_basis(basis, resu, aut) + return resu + + def tensor(self, tensor_type, name=None, latex_name=None, sym=None, + antisym=None): + r""" + Construct a tensor on the free module ``self``. + + INPUT: + + - ``tensor_type`` -- pair ``(k, l)`` with ``k`` being the + contravariant rank and ``l`` the covariant rank + - ``name`` -- (default: ``None``) string; name given to the tensor + - ``latex_name`` -- (default: ``None``) string; LaTeX symbol to + denote the tensor; if none is provided, the LaTeX symbol is set + to ``name`` + - ``sym`` -- (default: ``None``) a symmetry or a list of symmetries + among the tensor arguments: each symmetry is described by a tuple + containing the positions of the involved arguments, with the + convention ``position = 0`` for the first argument. For instance: + + * ``sym = (0,1)`` for a symmetry between the 1st and 2nd arguments + * ``sym = [(0,2), (1,3,4)]`` for a symmetry between the 1st and 3rd + arguments and a symmetry between the 2nd, 4th and 5th arguments. + + - ``antisym`` -- (default: ``None``) antisymmetry or list of + antisymmetries among the arguments, with the same convention + as for ``sym`` + + OUTPUT: + + - instance of + :class:`~sage.tensor.modules.free_module_tensor.FreeModuleTensor` + representing the tensor defined on ``self`` with the provided + characteristics + + EXAMPLES: + + Tensors on a rank-3 free module:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: t = M.tensor((1,0), name='t') ; t + Element t of the Rank-3 free module M over the Integer Ring + sage: t = M.tensor((0,1), name='t') ; t + Linear form t on the Rank-3 free module M over the Integer Ring + sage: t = M.tensor((1,1), name='t') ; t + Type-(1,1) tensor t on the Rank-3 free module M over the Integer Ring + sage: t = M.tensor((0,2), name='t', sym=(0,1)) ; t + Symmetric bilinear form t on the + Rank-3 free module M over the Integer Ring + sage: t = M.tensor((0,2), name='t', antisym=(0,1)) ; t + Alternating form t of degree 2 on the + Rank-3 free module M over the Integer Ring + sage: t = M.tensor((1,2), name='t') ; t + Type-(1,2) tensor t on the Rank-3 free module M over the Integer Ring + + See :class:`~sage.tensor.modules.free_module_tensor.FreeModuleTensor` + for more examples and documentation. + + """ + # Special cases: + if tensor_type == (1,0): + return self.element_class(self, name=name, latex_name=latex_name) + elif tensor_type == (0,1): + return self.linear_form(name=name, latex_name=latex_name) + elif tensor_type[0] == 0 and tensor_type[1] > 1 and antisym: + if isinstance(antisym, list): + antisym0 = antisym[0] + else: + antisym0 = antisym + if len(antisym0) == tensor_type[1]: + return self.alternating_form(tensor_type[1], name=name, + latex_name=latex_name) + # Generic case: + return self.tensor_module(*tensor_type).element_class(self, + tensor_type, name=name, latex_name=latex_name, + sym=sym, antisym=antisym) + + def tensor_from_comp(self, tensor_type, comp, name=None, latex_name=None): + r""" + Construct a tensor on ``self`` from a set of components. + + The tensor symmetries are deduced from those of the components. + + INPUT: + + - ``tensor_type`` -- pair ``(k, l)`` with ``k`` being the + contravariant rank and ``l`` the covariant rank + - ``comp`` -- instance of :class:`~sage.tensor.modules.comp.Components` + representing the tensor components in a given basis + - ``name`` -- (default: ``None``) string; name given to the tensor + - ``latex_name`` -- (default: ``None``) string; LaTeX symbol to denote + the tensor; if none is provided, the LaTeX symbol is set to ``name`` + + OUTPUT: + + - instance of + :class:`~sage.tensor.modules.free_module_tensor.FreeModuleTensor` + representing the tensor defined on ``self`` with the provided + characteristics. + + EXAMPLES: + + Construction of a tensor of rank 1:: + + sage: from sage.tensor.modules.comp import Components, CompWithSym, CompFullySym, CompFullyAntiSym + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') ; e + Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer Ring + sage: c = Components(ZZ, e, 1) + sage: c[:] + [0, 0, 0] + sage: c[:] = [-1,4,2] + sage: t = M.tensor_from_comp((1,0), c) + sage: t + Element of the Rank-3 free module M over the Integer Ring + sage: t.display(e) + -e_0 + 4 e_1 + 2 e_2 + sage: t = M.tensor_from_comp((0,1), c) ; t + Linear form on the Rank-3 free module M over the Integer Ring + sage: t.display(e) + -e^0 + 4 e^1 + 2 e^2 + + Construction of a tensor of rank 2:: + + sage: c = CompFullySym(ZZ, e, 2) + sage: c[0,0], c[1,2] = 4, 5 + sage: t = M.tensor_from_comp((0,2), c) ; t + Symmetric bilinear form on the + Rank-3 free module M over the Integer Ring + sage: t.symmetries() + symmetry: (0, 1); no antisymmetry + sage: t.display(e) + 4 e^0*e^0 + 5 e^1*e^2 + 5 e^2*e^1 + sage: c = CompFullyAntiSym(ZZ, e, 2) + sage: c[0,1], c[1,2] = 4, 5 + sage: t = M.tensor_from_comp((0,2), c) ; t + Alternating form of degree 2 on the + Rank-3 free module M over the Integer Ring + sage: t.display(e) + 4 e^0/\e^1 + 5 e^1/\e^2 + + """ + from comp import CompWithSym, CompFullySym, CompFullyAntiSym + # + # 0/ Compatibility checks: + if comp._ring is not self._ring: + raise TypeError("the components are not defined on the same" + + " ring as the module") + if comp._frame not in self._known_bases: + raise TypeError("the components are not defined on a basis of" + + " the module") + if comp._nid != tensor_type[0] + tensor_type[1]: + raise TypeError("number of component indices not compatible with "+ + " the tensor type") + # + # 1/ Construction of the tensor: + if tensor_type == (1,0): + resu = self.element_class(self, name=name, latex_name=latex_name) + elif tensor_type == (0,1): + resu = self.linear_form(name=name, latex_name=latex_name) + elif tensor_type[0] == 0 and tensor_type[1] > 1 and \ + isinstance(comp, CompFullyAntiSym): + resu = self.alternating_form(tensor_type[1], name=name, + latex_name=latex_name) + else: + resu = self.tensor_module(*tensor_type).element_class(self, + tensor_type, name=name, latex_name=latex_name) + # Tensor symmetries deduced from those of comp: + if isinstance(comp, CompWithSym): + resu._sym = comp._sym + resu._antisym = comp._antisym + # + # 2/ Tensor components set to comp: + resu._components[comp._frame] = comp + # + return resu + + def alternating_form(self, degree, name=None, latex_name=None): + r""" + Construct an alternating form on the free module. + + INPUT: + + - ``degree`` -- the degree of the alternating form (i.e. its + tensor rank) + - ``name`` -- (default: ``None``) string; name given to the + alternating form + - ``latex_name`` -- (default: ``None``) string; LaTeX symbol to + denote the alternating form; if none is provided, the LaTeX symbol + is set to ``name`` + + OUTPUT: + + - instance of + :class:`~sage.tensor.modules.free_module_alt_form.FreeModuleAltForm` + + EXAMPLES: + + Alternating forms on a rank-3 module:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: a = M.alternating_form(2, 'a') ; a + Alternating form a of degree 2 on the + Rank-3 free module M over the Integer Ring + + The nonzero components in a given basis have to be set in a second + step, thereby fully specifying the alternating form:: + + sage: e = M.basis('e') ; e + Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer Ring + sage: a.set_comp(e)[0,1] = 2 + sage: a.set_comp(e)[1,2] = -3 + sage: a.display(e) + a = 2 e^0/\e^1 - 3 e^1/\e^2 + + An alternating form of degree 1 is a linear form:: + + sage: a = M.alternating_form(1, 'a') ; a + Linear form a on the Rank-3 free module M over the Integer Ring + + To construct such a form, it is preferable to call the method + :meth:`linear_form` instead:: + + sage: a = M.linear_form('a') ; a + Linear form a on the Rank-3 free module M over the Integer Ring + + See + :class:`~sage.tensor.modules.free_module_alt_form.FreeModuleAltForm` + for more documentation. + + """ + return self.dual_exterior_power(degree).element_class(self, degree, + name=name, latex_name=latex_name) + + def linear_form(self, name=None, latex_name=None): + r""" + Construct a linear form on the free module ``self``. + + A *linear form* on a free module `M` over a ring `R` is a map + `M \rightarrow R` that is linear. It can be viewed as a tensor of type + `(0,1)` on `M`. + + INPUT: + + - ``name`` -- (default: ``None``) string; name given to the linear + form + - ``latex_name`` -- (default: ``None``) string; LaTeX symbol to + denote the linear form; if none is provided, the LaTeX symbol + is set to ``name`` + + OUTPUT: + + - instance of + :class:`~sage.tensor.modules.free_module_alt_form.FreeModuleAltForm` + + EXAMPLES: + + Linear form on a rank-3 free module:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: a = M.linear_form('A') ; a + Linear form A on the Rank-3 free module M over the Integer Ring + sage: a[:] = [2,-1,3] # components w.r.t. the module's default basis (e) + sage: a.display() + A = 2 e^0 - e^1 + 3 e^2 + + A linear form maps module elements to ring elements:: + + sage: v = M([1,1,1]) + sage: a(v) + 4 + + Test of linearity:: + + sage: u = M([-5,-2,7]) + sage: a(3*u - 4*v) == 3*a(u) - 4*a(v) + True + + See + :class:`~sage.tensor.modules.free_module_alt_form.FreeModuleAltForm` + for more documentation. + + """ + return self.dual_exterior_power(1).element_class(self, 1, name=name, + latex_name=latex_name) + + def automorphism(self, matrix=None, basis=None, name=None, + latex_name=None): + r""" + Construct a module automorphism of ``self``. + + Denoting ``self`` by `M`, an automorphism of ``self`` is an element + of the general linear group `\mathrm{GL}(M)`. + + INPUT: + + - ``matrix`` -- (default: ``None``) matrix of size rank(M)*rank(M) + representing the automorphism with respect to ``basis``; + this entry can actually be any material from which a matrix of + elements of ``self`` base ring can be constructed; the *columns* of + ``matrix`` must be the components w.r.t. ``basis`` of + the images of the elements of ``basis``. If ``matrix`` is ``None``, + the automorphism has to be initialized afterwards by + method :meth:`~sage.tensor.modules.free_module_tensor.FreeModuleTensor.set_comp` + or via the operator []. + - ``basis`` -- (default: ``None``) basis of ``self`` defining the + matrix representation; if ``None`` the default basis of ``self`` is + assumed. + - ``name`` -- (default: ``None``) string; name given to the + automorphism + - ``latex_name`` -- (default: ``None``) string; LaTeX symbol to + denote the automorphism; if none is provided, the LaTeX symbol + is set to ``name`` + + OUTPUT: + + - instance of + :class:`~sage.tensor.modules.free_module_automorphism.FreeModuleAutomorphism` + + EXAMPLES: + + Automorphism of a rank-2 free `\ZZ`-module:: + + sage: M = FiniteRankFreeModule(ZZ, 2, name='M') + sage: e = M.basis('e') + sage: a = M.automorphism(matrix=[[1,2],[1,3]], basis=e, name='a') ; a + Automorphism a of the Rank-2 free module M over the Integer Ring + sage: a.parent() + General linear group of the Rank-2 free module M over the Integer Ring + sage: a.matrix(e) + [1 2] + [1 3] + + An automorphism is a tensor of type (1,1):: + + sage: a.tensor_type() + (1, 1) + sage: a.display(e) + a = e_0*e^0 + 2 e_0*e^1 + e_1*e^0 + 3 e_1*e^1 + + The automorphism components can be specified in a second step, as + components of a type-(1,1) tensor:: + + sage: a1 = M.automorphism(name='a') + sage: a1[e,:] = [[1,2],[1,3]] + sage: a1.matrix(e) + [1 2] + [1 3] + sage: a1 == a + True + + Component by component specification:: + + sage: a2 = M.automorphism(name='a') + sage: a2[0,0] = 1 # component set in the module's default basis (e) + sage: a2[0,1] = 2 + sage: a2[1,0] = 1 + sage: a2[1,1] = 3 + sage: a2.matrix(e) + [1 2] + [1 3] + sage: a2 == a + True + + See + :class:`~sage.tensor.modules.free_module_automorphism.FreeModuleAutomorphism` + for more documentation. + + """ + resu = self.general_linear_group().element_class(self, name=name, + latex_name=latex_name) + if matrix: + if basis is None: + basis = self.default_basis() + resu.set_comp(basis)[:] = matrix + return resu + + def sym_bilinear_form(self, name=None, latex_name=None): + r""" + Construct a symmetric bilinear form on the free module ``self``. + + INPUT: + + - ``name`` -- (default: ``None``) string; name given to the symmetric + bilinear form + - ``latex_name`` -- (default: ``None``) string; LaTeX symbol to + denote the symmetric bilinear form; if none is provided, the LaTeX + symbol is set to ``name`` + + OUTPUT: + + - instance of + :class:`~sage.tensor.modules.free_module_tensor.FreeModuleTensor` + of tensor type `(0,2)` and symmetric + + EXAMPLES: + + Symmetric bilinear form on a rank-3 free module:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: a = M.sym_bilinear_form('A') ; a + Symmetric bilinear form A on the + Rank-3 free module M over the Integer Ring + + A symmetric bilinear form is a type-(0,2) tensor that is symmetric:: + + sage: a.parent() + Free module of type-(0,2) tensors on the + Rank-3 free module M over the Integer Ring + sage: a.tensor_type() + (0, 2) + sage: a.tensor_rank() + 2 + sage: a.symmetries() + symmetry: (0, 1); no antisymmetry + + Components with respect to a given basis:: + + sage: e = M.basis('e') + sage: a[0,0], a[0,1], a[0,2] = 1, 2, 3 + sage: a[1,1], a[1,2] = 4, 5 + sage: a[2,2] = 6 + + Only independent components have been set; the other ones are + deduced by symmetry:: + + sage: a[1,0], a[2,0], a[2,1] + (2, 3, 5) + sage: a[:] + [1 2 3] + [2 4 5] + [3 5 6] + + A symmetric bilinear form acts on pairs of module elements:: + + sage: u = M([2,-1,3]) ; v = M([-2,4,1]) + sage: a(u,v) + 61 + sage: a(v,u) == a(u,v) + True + + The sum of two symmetric bilinear forms is another symmetric bilinear + form:: + + sage: b = M.sym_bilinear_form('B') + sage: b[0,0], b[0,1], b[1,2] = -2, 1, -3 + sage: s = a + b ; s + Symmetric bilinear form A+B on the + Rank-3 free module M over the Integer Ring + sage: a[:], b[:], s[:] + ( + [1 2 3] [-2 1 0] [-1 3 3] + [2 4 5] [ 1 0 -3] [ 3 4 2] + [3 5 6], [ 0 -3 0], [ 3 2 6] + ) + + Adding a symmetric bilinear from with a non-symmetric one results in a + generic type-`(0,2)` tensor:: + + sage: c = M.tensor((0,2), name='C') + sage: c[0,1] = 4 + sage: s = a + c ; s + Type-(0,2) tensor A+C on the Rank-3 free module M over the Integer Ring + sage: s.symmetries() + no symmetry; no antisymmetry + sage: s[:] + [1 6 3] + [2 4 5] + [3 5 6] + + See :class:`~sage.tensor.modules.free_module_tensor.FreeModuleTensor` + for more documentation. + + """ + return self.tensor_module(0,2).element_class(self, (0,2), name=name, + latex_name=latex_name, sym=(0,1)) + + #### End of methods to be redefined by derived classes #### + + def _latex_(self): + r""" + LaTeX representation of ``self``. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: M._latex_() + 'M' + sage: latex(M) + M + sage: M1 = FiniteRankFreeModule(ZZ, 3, name='M', latex_name=r'\mathcal{M}') + sage: M1._latex_() + '\\mathcal{M}' + sage: latex(M1) + \mathcal{M} + + """ + if self._latex_name is None: + return r'\mbox{' + str(self) + r'}' + else: + return self._latex_name + + def rank(self): + r""" + Return the rank of the free module ``self``. + + Since the ring over which ``self`` is built is assumed to be + commutative (and hence has the invariant basis number property), the + rank is defined uniquely, as the cardinality of any basis of ``self``. + + EXAMPLES: + + Rank of free modules over `\ZZ`:: + + sage: M = FiniteRankFreeModule(ZZ, 3) + sage: M.rank() + 3 + sage: M.tensor_module(0,1).rank() + 3 + sage: M.tensor_module(0,2).rank() + 9 + sage: M.tensor_module(1,0).rank() + 3 + sage: M.tensor_module(1,1).rank() + 9 + sage: M.tensor_module(1,2).rank() + 27 + sage: M.tensor_module(2,2).rank() + 81 + + """ + return self._rank + + def zero(self): + r""" + Return the zero element of ``self``. + + EXAMPLES: + + Zero elements of free modules over `\ZZ`:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: M.zero() + Element zero of the Rank-3 free module M over the Integer Ring + sage: M.zero().parent() is M + True + sage: M.zero() is M(0) + True + sage: T = M.tensor_module(1,1) + sage: T.zero() + Type-(1,1) tensor zero on the Rank-3 free module M over the Integer Ring + sage: T.zero().parent() is T + True + sage: T.zero() is T(0) + True + + Components of the zero element with respect to some basis:: + + sage: e = M.basis('e') + sage: M.zero()[e,:] + [0, 0, 0] + sage: all(M.zero()[e,i] == M.base_ring().zero() for i in M.irange()) + True + sage: T.zero()[e,:] + [0 0 0] + [0 0 0] + [0 0 0] + sage: M.tensor_module(1,2).zero()[e,:] + [[[0, 0, 0], [0, 0, 0], [0, 0, 0]], + [[0, 0, 0], [0, 0, 0], [0, 0, 0]], + [[0, 0, 0], [0, 0, 0], [0, 0, 0]]] + + """ + return self._zero_element + + def dual(self): + r""" + Return the dual module of ``self``. + + EXAMPLE: + + Dual of a free module over `\ZZ`:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: M.dual() + Dual of the Rank-3 free module M over the Integer Ring + sage: latex(M.dual()) + M^* + + The dual is a free module of the same rank as M:: + + sage: isinstance(M.dual(), FiniteRankFreeModule) + True + sage: M.dual().rank() + 3 + + It is formed by alternating forms of degree 1, i.e. linear forms:: + + sage: M.dual() is M.dual_exterior_power(1) + True + sage: M.dual().an_element() + Linear form on the Rank-3 free module M over the Integer Ring + sage: a = M.linear_form() + sage: a in M.dual() + True + + The elements of a dual basis belong of course to the dual module:: + + sage: e = M.basis('e') + sage: e.dual_basis()[0] in M.dual() + True + + """ + return self.dual_exterior_power(1) + + def irange(self, start=None): + r""" + Single index generator, labelling the elements of a basis of ``self``. + + INPUT: + + - ``start`` -- (default: ``None``) integer; initial value of the + index; if none is provided, ``self._sindex`` is assumed + + OUTPUT: + + - an iterable index, starting from ``start`` and ending at + ``self._sindex + self.rank() - 1`` + + EXAMPLES: + + Index range on a rank-3 module:: + + sage: M = FiniteRankFreeModule(ZZ, 3) + sage: for i in M.irange(): print i, + 0 1 2 + sage: for i in M.irange(start=1): print i, + 1 2 + + The default starting value corresponds to the parameter ``start_index`` + provided at the module construction (the default value being 0):: + + sage: M1 = FiniteRankFreeModule(ZZ, 3, start_index=1) + sage: for i in M1.irange(): print i, + 1 2 3 + sage: M2 = FiniteRankFreeModule(ZZ, 3, start_index=-4) + sage: for i in M2.irange(): print i, + -4 -3 -2 + + """ + si = self._sindex + imax = self._rank + si + if start is None: + i = si + else: + i = start + while i < imax: + yield i + i += 1 + + def default_basis(self): + r""" + Return the default basis of the free module ``self``. + + The *default basis* is simply a basis whose name can be skipped in + methods requiring a basis as an argument. By default, it is the first + basis introduced on the module. It can be changed by the method + :meth:`set_default_basis`. + + OUTPUT: + + - instance of + :class:`~sage.tensor.modules.free_module_basis.FreeModuleBasis` + + EXAMPLES: + + At the module construction, no default basis is assumed:: + + sage: M = FiniteRankFreeModule(ZZ, 2, name='M', start_index=1) + sage: M.default_basis() + No default basis has been defined on the + Rank-2 free module M over the Integer Ring + + The first defined basis becomes the default one:: + + sage: e = M.basis('e') ; e + Basis (e_1,e_2) on the Rank-2 free module M over the Integer Ring + sage: M.default_basis() + Basis (e_1,e_2) on the Rank-2 free module M over the Integer Ring + sage: f = M.basis('f') ; f + Basis (f_1,f_2) on the Rank-2 free module M over the Integer Ring + sage: M.default_basis() + Basis (e_1,e_2) on the Rank-2 free module M over the Integer Ring + + """ + if self._def_basis is None: + print("No default basis has been defined on the {}".format(self)) + return self._def_basis + + def set_default_basis(self, basis): + r""" + Sets the default basis of ``self``. + + The *default basis* is simply a basis whose name can be skipped in + methods requiring a basis as an argument. By default, it is the first + basis introduced on the module. + + INPUT: + + - ``basis`` -- instance of + :class:`~sage.tensor.modules.free_module_basis.FreeModuleBasis` + representing a basis on ``self`` + + EXAMPLES: + + Changing the default basis on a rank-3 free module:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M', start_index=1) + sage: e = M.basis('e') ; e + Basis (e_1,e_2,e_3) on the Rank-3 free module M over the Integer Ring + sage: f = M.basis('f') ; f + Basis (f_1,f_2,f_3) on the Rank-3 free module M over the Integer Ring + sage: M.default_basis() + Basis (e_1,e_2,e_3) on the Rank-3 free module M over the Integer Ring + sage: M.set_default_basis(f) + sage: M.default_basis() + Basis (f_1,f_2,f_3) on the Rank-3 free module M over the Integer Ring + + """ + from free_module_basis import FreeModuleBasis + if not isinstance(basis, FreeModuleBasis): + raise TypeError("the argument is not a free module basis") + if basis._fmodule is not self: + raise ValueError("the basis is not defined on the current module") + self._def_basis = basis + + def print_bases(self): + r""" + Display the bases that have been defined on the free module ``self``. + + Use the method :meth:`bases` to get the raw list of bases. + + EXAMPLES: + + Bases on a rank-4 free module:: + + sage: M = FiniteRankFreeModule(ZZ, 4, name='M', start_index=1) + sage: M.print_bases() + No basis has been defined on the + Rank-4 free module M over the Integer Ring + sage: e = M.basis('e') + sage: M.print_bases() + Bases defined on the Rank-4 free module M over the Integer Ring: + - (e_1,e_2,e_3,e_4) (default basis) + sage: f = M.basis('f') + sage: M.print_bases() + Bases defined on the Rank-4 free module M over the Integer Ring: + - (e_1,e_2,e_3,e_4) (default basis) + - (f_1,f_2,f_3,f_4) + sage: M.set_default_basis(f) + sage: M.print_bases() + Bases defined on the Rank-4 free module M over the Integer Ring: + - (e_1,e_2,e_3,e_4) + - (f_1,f_2,f_3,f_4) (default basis) + + """ + if not self._known_bases: + print("No basis has been defined on the {}".format(self)) + else: + print("Bases defined on the {}:".format(self)) + for basis in self._known_bases: + item = " - " + basis._name + if basis is self._def_basis: + item += " (default basis)" + print(item) + + def bases(self): + r""" + Return the list of bases that have been defined on the free module + ``self``. + + Use the method :meth:`print_bases` to get a formatted output with more + information. + + OUTPUT: + + - list of instances of class + :class:`~sage.tensor.modules.free_module_basis.FreeModuleBasis` + + EXAMPLES: + + Bases on a rank-3 free module:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M_3', start_index=1) + sage: M.bases() + [] + sage: e = M.basis('e') + sage: M.bases() + [Basis (e_1,e_2,e_3) on the Rank-3 free module M_3 over the Integer Ring] + sage: f = M.basis('f') + sage: M.bases() + [Basis (e_1,e_2,e_3) on the Rank-3 free module M_3 over the Integer Ring, + Basis (f_1,f_2,f_3) on the Rank-3 free module M_3 over the Integer Ring] + + """ + return self._known_bases + + def change_of_basis(self, basis1, basis2): + r""" + Return a module automorphism linking two bases defined on the free + module ``self``. + + If the automorphism has not been recorded yet (in the internal + dictionary ``self._basis_changes``), it is computed by transitivity, + i.e. by performing products of recorded changes of basis. + + INPUT: + + - ``basis1`` -- a basis of ``self``, denoted `(e_i)` below + - ``basis2`` -- a basis of ``self``, denoted `(f_i)` below + + OUTPUT: + + - instance of + :class:`~sage.tensor.modules.free_module_automorphism.FreeModuleAutomorphism` + describing the automorphism `P` that relates the basis `(e_i)` to the + basis `(f_i)` according to `f_i = P(e_i)` + + EXAMPLES: + + Changes of basis on a rank-2 free module:: + + sage: FiniteRankFreeModule._clear_cache_() # for doctests only + sage: M = FiniteRankFreeModule(ZZ, 2, name='M', start_index=1) + sage: e = M.basis('e') + sage: f = M.basis('f', from_family=(e[1]+2*e[2], e[1]+3*e[2])) + sage: P = M.change_of_basis(e,f) ; P + Automorphism of the Rank-2 free module M over the Integer Ring + sage: P.matrix(e) + [1 1] + [2 3] + + Note that the columns of this matrix contain the components of the + elements of basis ``f`` w.r.t. to basis ``e``:: + + sage: f[1].display(e) + f_1 = e_1 + 2 e_2 + sage: f[2].display(e) + f_2 = e_1 + 3 e_2 + + The change of basis is cached:: + + sage: P is M.change_of_basis(e,f) + True + + Check of the change-of-basis automorphism:: + + sage: f[1] == P(e[1]) + True + sage: f[2] == P(e[2]) + True + + Check of the reverse change of basis:: + + sage: M.change_of_basis(f,e) == P^(-1) + True + + We have of course:: + + sage: M.change_of_basis(e,e) + Identity map of the Rank-2 free module M over the Integer Ring + sage: M.change_of_basis(e,e) is M.identity_map() + True + + Let us introduce a third basis on ``M``:: + + sage: h = M.basis('h', from_family=(3*e[1]+4*e[2], 5*e[1]+7*e[2])) + + The change of basis ``e`` --> ``h`` has been recorded directly from the + definition of ``h``:: + + sage: Q = M.change_of_basis(e,h) ; Q.matrix(e) + [3 5] + [4 7] + + The change of basis ``f`` --> ``h`` is computed by transitivity, i.e. + from the changes of basis ``f`` --> ``e`` and ``e`` --> ``h``:: + + sage: R = M.change_of_basis(f,h) ; R + Automorphism of the Rank-2 free module M over the Integer Ring + sage: R.matrix(e) + [-1 2] + [-2 3] + sage: R.matrix(f) + [ 5 8] + [-2 -3] + + Let us check that ``R`` is indeed the change of basis ``f`` --> ``h``:: + + sage: h[1] == R(f[1]) + True + sage: h[2] == R(f[2]) + True + + A related check is:: + + sage: R == Q*P^(-1) + True + + """ + if basis1 == basis2: + return self.identity_map() + bc = self._basis_changes + if (basis1, basis2) not in bc: + if basis1 not in self._known_bases: + raise TypeError("{} is not a basis of the {}".format(basis1, + self)) + if basis2 not in self._known_bases: + raise TypeError("{} is not a basis of the {}".format(basis2, + self)) + # Is the inverse already registred ? + if (basis2, basis1) in bc: + inv = bc[(basis2, basis1)].inverse() + bc[(basis1, basis2)] = inv + return inv + # Search for a third basis, basis say, such that either the changes + # basis1 --> basis and basis --> basis2 + # or + # basis2 --> basis and basis --> basis1 + # are known: + for basis in self._known_bases: + if (basis1, basis) in bc and (basis, basis2) in bc: + transf = bc[(basis, basis2)] * bc[(basis1, basis)] + bc[(basis1, basis2)] = transf + bc[(basis2, basis1)] = transf.inverse() + break + if (basis2, basis) in bc and (basis, basis1) in bc: + inv = bc[(basis, basis1)] * bc[(basis2, basis)] + bc[(basis2, basis1)] = inv + bc[(basis1, basis2)] = inv.inverse() + break + else: + raise ValueError(("the change of basis from '{!r}' to '{!r}'" + + " cannot be computed" + ).format(basis1, basis2)) + return bc[(basis1, basis2)] + + def set_change_of_basis(self, basis1, basis2, change_of_basis, + compute_inverse=True): + r""" + Relates two bases by an automorphism of ``self``. + + This updates the internal dictionary ``self._basis_changes``. + + INPUT: + + - ``basis1`` -- basis 1, denoted `(e_i)` below + - ``basis2`` -- basis 2, denoted `(f_i)` below + - ``change_of_basis`` -- instance of class + :class:`~sage.tensor.modules.free_module_automorphism.FreeModuleAutomorphism` + describing the automorphism `P` that relates the basis `(e_i)` to + the basis `(f_i)` according to `f_i = P(e_i)` + - ``compute_inverse`` (default: ``True``) -- if set to ``True``, the + inverse automorphism is computed and the change from basis `(f_i)` + to `(e_i)` is set to it in the internal dictionary + ``self._basis_changes`` + + EXAMPLES: + + Defining a change of basis on a rank-2 free module:: + + sage: M = FiniteRankFreeModule(QQ, 2, name='M') + sage: e = M.basis('e') + sage: f = M.basis('f') + sage: a = M.automorphism() + sage: a[:] = [[1, 2], [-1, 3]] + sage: M.set_change_of_basis(e, f, a) + + The change of basis and its inverse have been recorded:: + + sage: M.change_of_basis(e,f).matrix(e) + [ 1 2] + [-1 3] + sage: M.change_of_basis(f,e).matrix(e) + [ 3/5 -2/5] + [ 1/5 1/5] + + and are effective:: + + sage: f[0].display(e) + f_0 = e_0 - e_1 + sage: e[0].display(f) + e_0 = 3/5 f_0 + 1/5 f_1 + + """ + if basis1 not in self._known_bases: + raise TypeError("{} is not a basis of the {}".format(basis1, + self)) + if basis2 not in self._known_bases: + raise TypeError("{} is not a basis of the {}".format(basis2, + self)) + if change_of_basis not in self.general_linear_group(): + raise TypeError("{} is not an automorphism of the {}".format( + change_of_basis, self)) + self._basis_changes[(basis1, basis2)] = change_of_basis + if compute_inverse: + self._basis_changes[(basis2, basis1)] = change_of_basis.inverse() + + def hom(self, codomain, matrix_rep, bases=None, name=None, + latex_name=None): + r""" + Homomorphism from ``self`` to a free module. + + Define a module homomorphism + + .. MATH:: + + \phi:\ M \longrightarrow N, + + where `M` is ``self`` and `N` is a free module of finite rank + over the same ring `R` as ``self``. + + .. NOTE:: + + This method is a redefinition of + :meth:`sage.structure.parent.Parent.hom` because the latter assumes + that ``self`` has some privileged generators, while an instance of + :class:`FiniteRankFreeModule` has no privileged basis. + + INPUT: + + - ``codomain`` -- the target module `N` + - ``matrix_rep`` -- matrix of size rank(N)*rank(M) representing the + homomorphism with respect to the pair of bases defined by ``bases``; + this entry can actually be any material from which a matrix of + elements of `R` can be constructed; the *columns* of + ``matrix_rep`` must be the components w.r.t. ``basis_N`` of + the images of the elements of ``basis_M``. + - ``bases`` -- (default: ``None``) pair ``(basis_M, basis_N)`` defining + the matrix representation, ``basis_M`` being a basis of ``self`` and + ``basis_N`` a basis of module `N` ; if None the pair formed by the + default bases of each module is assumed. + - ``name`` -- (default: ``None``) string; name given to the + homomorphism + - ``latex_name`` -- (default: ``None``) string; LaTeX symbol to denote + the homomorphism; if None, ``name`` will be used. + + OUTPUT: + + - the homomorphism `\phi: M \rightarrow N` corresponding to the given + specifications, as an instance of + :class:`~sage.tensor.modules.free_module_morphism.FiniteRankFreeModuleMorphism` + + EXAMPLES: + + Homomorphism between two free modules over `\ZZ`:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: N = FiniteRankFreeModule(ZZ, 2, name='N') + sage: e = M.basis('e') + sage: f = N.basis('f') + sage: phi = M.hom(N, [[-1,2,0], [5,1,2]]) ; phi + Generic morphism: + From: Rank-3 free module M over the Integer Ring + To: Rank-2 free module N over the Integer Ring + + Homomorphism defined by a matrix w.r.t. bases that are not the + default ones:: + + sage: ep = M.basis('ep', latex_symbol=r"e'") + sage: fp = N.basis('fp', latex_symbol=r"f'") + sage: phi = M.hom(N, [[3,2,1], [1,2,3]], bases=(ep, fp)) ; phi + Generic morphism: + From: Rank-3 free module M over the Integer Ring + To: Rank-2 free module N over the Integer Ring + + Call with all arguments specified:: + + sage: phi = M.hom(N, [[3,2,1], [1,2,3]], bases=(ep, fp), + ....: name='phi', latex_name=r'\phi') + + The parent:: + + sage: phi.parent() is Hom(M,N) + True + + See class + :class:`~sage.tensor.modules.free_module_morphism.FiniteRankFreeModuleMorphism` + for more documentation. + + """ + from sage.categories.homset import Hom + homset = Hom(self, codomain) + return homset(matrix_rep, bases=bases, name=name, + latex_name=latex_name) + + def endomorphism(self, matrix_rep, basis=None, name=None, latex_name=None): + r""" + Contruct an endomorphism of the free module ``self``. + + The returned object is a module morphism `\phi: M \rightarrow M`, + where `M` is ``self``. + + INPUT: + + - ``matrix_rep`` -- matrix of size rank(M)*rank(M) representing the + endomorphism with respect to ``basis``; + this entry can actually be any material from which a matrix of + elements of ``self`` base ring can be constructed; the *columns* of + ``matrix_rep`` must be the components w.r.t. ``basis`` of + the images of the elements of ``basis``. + - ``basis`` -- (default: ``None``) basis of ``self`` defining the + matrix representation; if None the default basis of ``self`` is + assumed. + - ``name`` -- (default: ``None``) string; name given to the + endomorphism + - ``latex_name`` -- (default: ``None``) string; LaTeX symbol to denote + the endomorphism; if none is provided, ``name`` will be used. + + OUTPUT: + + - the endomorphism `\phi: M \rightarrow M` corresponding to the given + specifications, as an instance of + :class:`~sage.tensor.modules.free_module_morphism.FiniteRankFreeModuleMorphism` + + EXAMPLES: + + Construction of an endomorphism with minimal data (module's default + basis and no name):: + + sage: M = FiniteRankFreeModule(ZZ, 2, name='M') + sage: e = M.basis('e') + sage: phi = M.endomorphism([[1,-2], [-3,4]]) ; phi + Generic endomorphism of Rank-2 free module M over the Integer Ring + sage: phi.matrix() # matrix w.r.t the default basis + [ 1 -2] + [-3 4] + + Construction with full list of arguments (matrix given a basis + different from the default one):: + + sage: a = M.automorphism() ; a[0,1], a[1,0] = 1, -1 + sage: ep = e.new_basis(a, 'ep', latex_symbol="e'") + sage: phi = M.endomorphism([[1,-2], [-3,4]], basis=ep, name='phi', + ....: latex_name=r'\phi') + sage: phi + Generic endomorphism of Rank-2 free module M over the Integer Ring + sage: phi.matrix(ep) # the input matrix + [ 1 -2] + [-3 4] + sage: phi.matrix() # matrix w.r.t the default basis + [4 3] + [2 1] + + See :class:`~sage.tensor.modules.free_module_morphism.FiniteRankFreeModuleMorphism` + for more documentation. + + """ + from sage.categories.homset import End + if basis is None: + basis = self.default_basis() + return End(self)(matrix_rep, bases=(basis,basis), name=name, + latex_name=latex_name) + + def identity_map(self, name='Id', latex_name=None): + r""" + Return the identity map of the free module ``self``. + + INPUT: + + - ``name`` -- (string; default: 'Id') name given to the identity + identity map + - ``latex_name`` -- (string; default: ``None``) LaTeX symbol to denote + the identity map; if none is provided, the LaTeX symbol is set to + '\mathrm{Id}' if ``name`` is 'Id' and to ``name`` otherwise + + OUTPUT: + + - the identity map of ``self`` as an instance of + :class:`~sage.tensor.modules.free_module_automorphism.FreeModuleAutomorphism` + + EXAMPLES: + + Identity map of a rank-3 `\ZZ`-module:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: Id = M.identity_map() ; Id + Identity map of the Rank-3 free module M over the Integer Ring + sage: Id.parent() + General linear group of the Rank-3 free module M over the Integer Ring + sage: Id.matrix(e) + [1 0 0] + [0 1 0] + [0 0 1] + + The default LaTeX symbol:: + + sage: latex(Id) + \mathrm{Id} + + It can be changed by means of the method + :meth:`~sage.tensor.modules.free_module_tensor.FreeModuleTensor.set_name`:: + + sage: Id.set_name(latex_name=r'\mathrm{1}_M') + sage: latex(Id) + \mathrm{1}_M + + The identity map is actually the identity element of GL(M):: + + sage: Id is M.general_linear_group().one() + True + + It is also a tensor of type-(1,1) on M:: + + sage: Id.tensor_type() + (1, 1) + sage: Id.comp(e) + Kronecker delta of size 3x3 + sage: Id[:] + [1 0 0] + [0 1 0] + [0 0 1] + + Example with a LaTeX symbol different from the default one and set + at the creation of the object:: + + sage: N = FiniteRankFreeModule(ZZ, 3, name='N') + sage: f = N.basis('f') + sage: Id = N.identity_map(name='Id_N', latex_name=r'\mathrm{Id}_N') + sage: Id + Identity map of the Rank-3 free module N over the Integer Ring + sage: latex(Id) + \mathrm{Id}_N + + """ + if self._identity_map is None: + self._identity_map = self.general_linear_group().one() + if name != 'Id': + if latex_name is None: + latex_name = name + self._identity_map.set_name(name=name, latex_name=latex_name) + return self._identity_map diff --git a/src/sage/tensor/modules/format_utilities.py b/src/sage/tensor/modules/format_utilities.py new file mode 100644 index 00000000000..b634a0636f5 --- /dev/null +++ b/src/sage/tensor/modules/format_utilities.py @@ -0,0 +1,339 @@ +r""" +Formatting utilities + +This module defines helper functions that are not class methods. + +AUTHORS: + +- Eric Gourgoulhon, Michal Bejger (2014-2015): initial version +- Joris Vankerschaver (2010): for the function :func:`is_atomic()` + +""" + +#****************************************************************************** +# Copyright (C) 2015 Eric Gourgoulhon +# Copyright (C) 2015 Michal Bejger +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# http://www.gnu.org/licenses/ +#****************************************************************************** + +from sage.structure.sage_object import SageObject + +def is_atomic(expression): + r""" + Helper function to check whether some LaTeX expression is atomic. + + Adapted from method + :meth:`~sage.tensor.differential_form_element.DifferentialFormFormatter._is_atomic` + of class + :class:`~sage.tensor.differential_form_element.DifferentialFormFormatter` + written by Joris Vankerschaver (2010). + + INPUT: + + - ``expression`` -- string representing the expression (e.g. LaTeX string) + + OUTPUT: + + - ``True`` if additive operations are enclosed in parentheses and + ``False`` otherwise. + + EXAMPLES:: + + sage: from sage.tensor.modules.format_utilities import is_atomic + sage: is_atomic("2*x") + True + sage: is_atomic("2+x") + False + sage: is_atomic("(2+x)") + True + + """ + if not isinstance(expression, basestring): + raise TypeError("The argument must be a string") + level = 0 + for n, c in enumerate(expression): + if c == '(': + level += 1 + elif c == ')': + level -= 1 + if c == '+' or c == '-': + if level == 0 and n > 0: + return False + return True + + +def is_atomic_wedge_txt(expression): + r""" + Helper function to check whether some text-formatted expression is atomic + in terms of wedge products. + + Adapted from method + :meth:`~sage.tensor.differential_form_element.DifferentialFormFormatter._is_atomic` + of class + :class:`~sage.tensor.differential_form_element.DifferentialFormFormatter` + written by Joris Vankerschaver (2010). + + INPUT: + + - ``expression`` -- string representing the text-formatted expression + + OUTPUT: + + - ``True`` if wedge products are enclosed in parentheses and + ``False`` otherwise. + + EXAMPLES:: + + sage: from sage.tensor.modules.format_utilities import is_atomic_wedge_txt + sage: is_atomic_wedge_txt("a") + True + sage: is_atomic_wedge_txt(r"a/\b") + False + sage: is_atomic_wedge_txt(r"(a/\b)") + True + sage: is_atomic_wedge_txt(r"(a/\b)/\c") + False + sage: is_atomic_wedge_txt(r"(a/\b/\c)") + True + + """ + if not isinstance(expression, basestring): + raise TypeError("The argument must be a string.") + level = 0 + for n, c in enumerate(expression): + if c == '(': + level += 1 + elif c == ')': + level -= 1 + if c == '/' and expression[n+1:n+2] == '\\': + if level == 0 and n > 0: + return False + return True + + +def is_atomic_wedge_latex(expression): + r""" + Helper function to check whether LaTeX-formatted expression is atomic in + terms of wedge products. + + Adapted from method + :meth:`~sage.tensor.differential_form_element.DifferentialFormFormatter._is_atomic` + of class + :class:`~sage.tensor.differential_form_element.DifferentialFormFormatter` + written by Joris Vankerschaver (2010). + + INPUT: + + - ``expression`` -- string representing the LaTeX expression + + OUTPUT: + + - ``True`` if wedge products are enclosed in parentheses and + ``False`` otherwise. + + EXAMPLES:: + + sage: from sage.tensor.modules.format_utilities import is_atomic_wedge_latex + sage: is_atomic_wedge_latex(r"a") + True + sage: is_atomic_wedge_latex(r"a\wedge b") + False + sage: is_atomic_wedge_latex(r"(a\wedge b)") + True + sage: is_atomic_wedge_latex(r"(a\wedge b)\wedge c") + False + sage: is_atomic_wedge_latex(r"((a\wedge b)\wedge c)") + True + sage: is_atomic_wedge_latex(r"(a\wedge b\wedge c)") + True + sage: is_atomic_wedge_latex(r"\omega\wedge\theta") + False + sage: is_atomic_wedge_latex(r"(\omega\wedge\theta)") + True + sage: is_atomic_wedge_latex(r"\omega\wedge(\theta+a)") + False + + """ + if not isinstance(expression, basestring): + raise TypeError("The argument must be a string.") + level = 0 + for n, c in enumerate(expression): + if c == '(': + level += 1 + elif c == ')': + level -= 1 + if c == '\\' and expression[n+1:n+6] == 'wedge': + if level == 0 and n > 0: + return False + return True + + +def format_mul_txt(name1, operator, name2): + r""" + Helper function for text-formatted names of results of multiplication or + tensor product. + + EXAMPLES:: + + sage: from sage.tensor.modules.format_utilities import format_mul_txt + sage: format_mul_txt('a', '*', 'b') + 'a*b' + sage: format_mul_txt('a+b', '*', 'c') + '(a+b)*c' + sage: format_mul_txt('a', '*', 'b+c') + 'a*(b+c)' + sage: format_mul_txt('a+b', '*', 'c+d') + '(a+b)*(c+d)' + sage: format_mul_txt(None, '*', 'b') + sage: format_mul_txt('a', '*', None) + + """ + if name1 is None or name2 is None: + return None + if not is_atomic(name1) or not is_atomic_wedge_txt(name1): + name1 = '(' + name1 + ')' + if not is_atomic(name2) or not is_atomic_wedge_txt(name2): + name2 = '(' + name2 + ')' + return name1 + operator + name2 + + +def format_mul_latex(name1, operator, name2): + r""" + Helper function for LaTeX names of results of multiplication or tensor + product. + + EXAMPLES:: + + sage: from sage.tensor.modules.format_utilities import format_mul_latex + sage: format_mul_latex('a', '*', 'b') + 'a*b' + sage: format_mul_latex('a+b', '*', 'c') + '\\left(a+b\\right)*c' + sage: format_mul_latex('a', '*', 'b+c') + 'a*\\left(b+c\\right)' + sage: format_mul_latex('a+b', '*', 'c+d') + '\\left(a+b\\right)*\\left(c+d\\right)' + sage: format_mul_latex(None, '*', 'b') + sage: format_mul_latex('a', '*', None) + + """ + if name1 is None or name2 is None: + return None + if not is_atomic(name1) or not is_atomic_wedge_latex(name1): + name1 = r'\left(' + name1 + r'\right)' + if not is_atomic(name2) or not is_atomic_wedge_latex(name2): + name2 = r'\left(' + name2 + r'\right)' + return name1 + operator + name2 + + +def format_unop_txt(operator, name): + r""" + Helper function for text-formatted names of results of unary operator. + + EXAMPLES:: + + sage: from sage.tensor.modules.format_utilities import format_unop_txt + sage: format_unop_txt('-', 'a') + '-a' + sage: format_unop_txt('-', 'a+b') + '-(a+b)' + sage: format_unop_txt('-', '(a+b)') + '-(a+b)' + sage: format_unop_txt('-', None) + + """ + if name is None: + return None + if not is_atomic(name) or not is_atomic_wedge_txt(name): + #!# is_atomic_otimes_txt should be added + name = '(' + name + ')' + return operator + name + + +def format_unop_latex(operator, name): + r""" + Helper function for LaTeX names of results of unary operator. + + EXAMPLES:: + + sage: from sage.tensor.modules.format_utilities import format_unop_latex + sage: format_unop_latex('-', 'a') + '-a' + sage: format_unop_latex('-', 'a+b') + '-\\left(a+b\\right)' + sage: format_unop_latex('-', '(a+b)') + '-(a+b)' + sage: format_unop_latex('-', None) + + """ + if name is None: + return None + if not is_atomic(name) or not is_atomic_wedge_latex(name): + #!# is_atomic_otimes_latex should be added + name = r'\left(' + name + r'\right)' + return operator + name + + +class FormattedExpansion(SageObject): + r""" + Helper class for displaying tensor expansions. + + EXAMPLES:: + + sage: from sage.tensor.modules.format_utilities import FormattedExpansion + sage: f = FormattedExpansion('v', r'\tilde v') + sage: f + v + sage: latex(f) + \tilde v + sage: f = FormattedExpansion('x/2', r'\frac{x}{2}') + sage: f + x/2 + sage: latex(f) + \frac{x}{2} + + """ + def __init__(self, txt=None, latex=None): + r""" + TESTS:: + + sage: from sage.tensor.modules.format_utilities import FormattedExpansion + sage: f = FormattedExpansion('v', r'\tilde v') + sage: f + v + + """ + self._txt = txt + self._latex = latex + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLES:: + + sage: from sage.tensor.modules.format_utilities import FormattedExpansion + sage: f = FormattedExpansion('v', r'\tilde v') + sage: f._repr_() + 'v' + + """ + return self._txt + + def _latex_(self): + r""" + Return a LaTeX representation of ``self``. + + EXAMPLE:: + + sage: from sage.tensor.modules.format_utilities import FormattedExpansion + sage: f = FormattedExpansion('v', r'\tilde v') + sage: f._latex_() + '\\tilde v' + + """ + return self._latex diff --git a/src/sage/tensor/modules/free_module_alt_form.py b/src/sage/tensor/modules/free_module_alt_form.py new file mode 100644 index 00000000000..12b602bc0a6 --- /dev/null +++ b/src/sage/tensor/modules/free_module_alt_form.py @@ -0,0 +1,661 @@ +r""" +Alternating forms on free modules + +Given a free module `M` of finite rank over a commutative ring `R` +and a positive integer `p`, an *alternating form of degree* `p` on `M` is +a map + +.. MATH:: + + a:\ \underbrace{M\times\cdots\times M}_{p\ \; \mbox{times}} + \longrightarrow R + +that (i) is multilinear and (ii) vanishes whenever any of two of its +arguments are equal. +An alternating form of degree `p` is a tensor on `M` of type `(0,p)`. + +Alternating forms are implemented via the class :class:`FreeModuleAltForm`, +which is a subclass of the generic tensor class +:class:`~sage.tensor.modules.free_module_tensor.FreeModuleTensor`. + +AUTHORS: + +- Eric Gourgoulhon, Michal Bejger (2014-2015): initial version + +REFERENCES: + +- Chap. 23 of R. Godement: *Algebra*, Hermann (Paris) / Houghton Mifflin + (Boston) (1968) +- Chap. 15 of S. Lang: *Algebra*, 3rd ed., Springer (New York) (2002) + +""" +#****************************************************************************** +# Copyright (C) 2015 Eric Gourgoulhon +# Copyright (C) 2015 Michal Bejger +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# http://www.gnu.org/licenses/ +#****************************************************************************** + +from sage.tensor.modules.free_module_tensor import FreeModuleTensor +from sage.tensor.modules.comp import Components, CompFullyAntiSym + +class FreeModuleAltForm(FreeModuleTensor): + r""" + Alternating form on a free module of finite rank over a commutative ring. + + This is a Sage *element* class, the corresponding *parent* class being + :class:`~sage.tensor.modules.ext_pow_free_module.ExtPowerFreeModule`. + + INPUT: + + - ``fmodule`` -- free module `M` of finite rank over a commutative ring + `R`, as an instance of + :class:`~sage.tensor.modules.finite_rank_free_module.FiniteRankFreeModule` + - ``degree`` -- positive integer; the degree `p` of the + alternating form (i.e. its tensor rank) + - ``name`` -- (default: ``None``) string; name given to the alternating + form + - ``latex_name`` -- (default: ``None``) string; LaTeX symbol to denote the + alternating form; if none is provided, ``name`` is used + + EXAMPLES: + + Alternating form of degree 2 on a rank-3 free module:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M', start_index=1) + sage: e = M.basis('e') + sage: a = M.alternating_form(2, name='a') ; a + Alternating form a of degree 2 on the + Rank-3 free module M over the Integer Ring + sage: type(a) + + sage: a.parent() + 2nd exterior power of the dual of the Rank-3 free module M over the Integer Ring + sage: a[1,2], a[2,3] = 4, -3 + sage: a.display(e) + a = 4 e^1/\e^2 - 3 e^2/\e^3 + + The alternating form acting on the basis elements:: + + sage: a(e[1],e[2]) + 4 + sage: a(e[1],e[3]) + 0 + sage: a(e[2],e[3]) + -3 + sage: a(e[2],e[1]) + -4 + + An alternating form of degree 1 is a linear form:: + + sage: b = M.linear_form('b') ; b + Linear form b on the Rank-3 free module M over the Integer Ring + sage: b[:] = [2,-1,3] # components w.r.t. the module's default basis (e) + + A linear form is a tensor of type (0,1):: + + sage: b.tensor_type() + (0, 1) + + It is an element of the dual module:: + + sage: b.parent() + Dual of the Rank-3 free module M over the Integer Ring + sage: b.parent() is M.dual() + True + + The members of a dual basis are linear forms:: + + sage: e.dual_basis()[1] + Linear form e^1 on the Rank-3 free module M over the Integer Ring + sage: e.dual_basis()[2] + Linear form e^2 on the Rank-3 free module M over the Integer Ring + sage: e.dual_basis()[3] + Linear form e^3 on the Rank-3 free module M over the Integer Ring + + Any linear form is expanded onto them:: + + sage: b.display(e) + b = 2 e^1 - e^2 + 3 e^3 + + In the above example, an equivalent writing would have been + ``b.display()``, since the basis ``e`` is the module's default basis. + A linear form maps module elements to ring elements:: + + sage: v = M([1,1,1]) + sage: b(v) + 4 + sage: b(v) in M.base_ring() + True + + Test of linearity:: + + sage: u = M([-5,-2,7]) + sage: b(3*u - 4*v) == 3*b(u) - 4*b(v) + True + + The standard tensor operations apply to alternating forms, like the + extraction of components with respect to a given basis:: + + sage: a[e,1,2] + 4 + sage: a[1,2] # since e is the module's default basis + 4 + sage: all( a[i,j] == - a[j,i] for i in {1,2,3} for j in {1,2,3} ) + True + + the tensor product:: + + sage: c = b*b ; c + Symmetric bilinear form b*b on the Rank-3 free module M over the + Integer Ring + sage: c.parent() + Free module of type-(0,2) tensors on the Rank-3 free module M over the + Integer Ring + sage: c.display(e) + b*b = 4 e^1*e^1 - 2 e^1*e^2 + 6 e^1*e^3 - 2 e^2*e^1 + e^2*e^2 + - 3 e^2*e^3 + 6 e^3*e^1 - 3 e^3*e^2 + 9 e^3*e^3 + + the contractions:: + + sage: s = a.contract(v) ; s + Linear form on the Rank-3 free module M over the Integer Ring + sage: s.parent() + Dual of the Rank-3 free module M over the Integer Ring + sage: s.display(e) + 4 e^1 - 7 e^2 + 3 e^3 + + or tensor arithmetics:: + + sage: s = 3*a + c ; s + Type-(0,2) tensor on the Rank-3 free module M over the Integer Ring + sage: s.parent() + Free module of type-(0,2) tensors on the Rank-3 free module M over the + Integer Ring + sage: s.display(e) + 4 e^1*e^1 + 10 e^1*e^2 + 6 e^1*e^3 - 14 e^2*e^1 + e^2*e^2 + - 12 e^2*e^3 + 6 e^3*e^1 + 6 e^3*e^2 + 9 e^3*e^3 + + Note that tensor arithmetics preserves the alternating character if both + operands are alternating:: + + sage: s = a - 2*a ; s + Alternating form of degree 2 on the Rank-3 free module M over the + Integer Ring + sage: s.parent() # note the difference with s = 3*a + c above + 2nd exterior power of the dual of the Rank-3 free module M over the + Integer Ring + sage: s == -a + True + + An operation specific to alternating forms is of course the exterior + product:: + + sage: s = a.wedge(b) ; s + Alternating form a/\b of degree 3 on the Rank-3 free module M over the + Integer Ring + sage: s.parent() + 3rd exterior power of the dual of the Rank-3 free module M over the + Integer Ring + sage: s.display(e) + a/\b = 6 e^1/\e^2/\e^3 + sage: s[1,2,3] == a[1,2]*b[3] + a[2,3]*b[1] + a[3,1]*b[2] + True + + The exterior product is nilpotent on linear forms:: + + sage: s = b.wedge(b) ; s + Alternating form b/\b of degree 2 on the Rank-3 free module M over the + Integer Ring + sage: s.display(e) + b/\b = 0 + + """ + def __init__(self, fmodule, degree, name=None, latex_name=None): + r""" + Initialize ``self``. + + TESTS:: + + sage: from sage.tensor.modules.free_module_alt_form import FreeModuleAltForm + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: a = FreeModuleAltForm(M, 2, name='a') + sage: a[e,0,1] = 2 + sage: TestSuite(a).run(skip="_test_category") # see below + + In the above test suite, _test_category fails because a is not an + instance of a.parent().category().element_class. Actually alternating + forms must be constructed via ExtPowerFreeModule.element_class and + not by a direct call to FreeModuleAltForm:: + + sage: a1 = M.dual_exterior_power(2).element_class(M, 2, name='a') + sage: a1[e,0,1] = 2 + sage: TestSuite(a1).run() + + """ + FreeModuleTensor.__init__(self, fmodule, (0,degree), name=name, + latex_name=latex_name, antisym=range(degree), + parent=fmodule.dual_exterior_power(degree)) + FreeModuleAltForm._init_derived(self) # initialization of derived + # quantities + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: M.alternating_form(1) + Linear form on the Rank-3 free module M over the Integer Ring + sage: M.alternating_form(1, name='a') + Linear form a on the Rank-3 free module M over the Integer Ring + sage: M.alternating_form(2) + Alternating form of degree 2 on the + Rank-3 free module M over the Integer Ring + sage: M.alternating_form(2, name='a') + Alternating form a of degree 2 on the + Rank-3 free module M over the Integer Ring + """ + if self._tensor_rank == 1: + description = "Linear form " + if self._name is not None: + description += self._name + " " + else: + description = "Alternating form " + if self._name is not None: + description += self._name + " " + description += "of degree {} ".format(self._tensor_rank) + description += "on the {}".format(self._fmodule) + return description + + def _init_derived(self): + r""" + Initialize the derived quantities. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: a = M.alternating_form(2) + sage: a._init_derived() + + """ + FreeModuleTensor._init_derived(self) + + def _del_derived(self): + r""" + Delete the derived quantities. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: a = M.alternating_form(2) + sage: a._del_derived() + + """ + FreeModuleTensor._del_derived(self) + + def _new_instance(self): + r""" + Create an instance of the same class as ``self``, on the same module + and of the same degree. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: a = M.alternating_form(1) + sage: a._new_instance() + Linear form on the Rank-3 free module M over the Integer Ring + sage: a._new_instance().parent() is a.parent() + True + sage: b = M.alternating_form(2, name='b') + sage: b._new_instance() + Alternating form of degree 2 on the + Rank-3 free module M over the Integer Ring + sage: b._new_instance().parent() is b.parent() + True + + """ + return self.__class__(self._fmodule, self._tensor_rank) + + def _new_comp(self, basis): + r""" + Create some (uninitialized) components of ``self`` in a given basis. + + This method, which is already implemented in + :meth:`FreeModuleTensor._new_comp`, is redefined here for efficiency. + + INPUT: + + - ``basis`` -- basis of the free module on which ``self`` is defined + + OUTPUT: + + - an instance of :class:`~sage.tensor.modules.comp.CompFullyAntiSym`, + or of :class:`~sage.tensor.modules.comp.Components` if + the degree of ``self`` is 1. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: a = M.alternating_form(2, name='a') + sage: a._new_comp(e) + Fully antisymmetric 2-indices components w.r.t. Basis (e_0,e_1,e_2) + on the Rank-3 free module M over the Integer Ring + sage: a = M.alternating_form(1) + sage: a._new_comp(e) + 1-index components w.r.t. Basis (e_0,e_1,e_2) on the Rank-3 free + module M over the Integer Ring + + """ + fmodule = self._fmodule # the base free module + if self._tensor_rank == 1: + return Components(fmodule._ring, basis, 1, + start_index=fmodule._sindex, + output_formatter=fmodule._output_formatter) + + return CompFullyAntiSym(fmodule._ring, basis, self._tensor_rank, + start_index=fmodule._sindex, + output_formatter=fmodule._output_formatter) + + def degree(self): + r""" + Return the degree of ``self``. + + EXAMPLE:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: a = M.alternating_form(2, name='a') + sage: a.degree() + 2 + + """ + return self._tensor_rank + + + def display(self, basis=None, format_spec=None): + r""" + Display the alternating form ``self`` in terms of its expansion w.r.t. + a given module basis. + + The expansion is actually performed onto exterior products of elements + of the cobasis (dual basis) associated with ``basis`` (see examples + below). The output is either text-formatted (console mode) or + LaTeX-formatted (notebook mode). + + INPUT: + + - ``basis`` -- (default: ``None``) basis of the free module with + respect to which the alternating form is expanded; if none is + provided, the module's default basis is assumed + - ``format_spec`` -- (default: ``None``) format specification passed + to ``self._fmodule._output_formatter`` to format the output + + EXAMPLES: + + Display of an alternating form of degree 1 (linear form) on a rank-3 + free module:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: e.dual_basis() + Dual basis (e^0,e^1,e^2) on the Rank-3 free module M over the Integer Ring + sage: a = M.linear_form('a', latex_name=r'\alpha') + sage: a[:] = [1,-3,4] + sage: a.display(e) + a = e^0 - 3 e^1 + 4 e^2 + sage: a.display() # a shortcut since e is M's default basis + a = e^0 - 3 e^1 + 4 e^2 + sage: latex(a.display()) # display in the notebook + \alpha = e^0 -3 e^1 + 4 e^2 + + A shortcut is ``disp()``:: + + sage: a.disp() + a = e^0 - 3 e^1 + 4 e^2 + + Display of an alternating form of degree 2 on a rank-3 free module:: + + sage: b = M.alternating_form(2, 'b', latex_name=r'\beta') + sage: b[0,1], b[0,2], b[1,2] = 3, 2, -1 + sage: b.display() + b = 3 e^0/\e^1 + 2 e^0/\e^2 - e^1/\e^2 + sage: latex(b.display()) # display in the notebook + \beta = 3 e^0\wedge e^1 + 2 e^0\wedge e^2 -e^1\wedge e^2 + + Display of an alternating form of degree 3 on a rank-3 free module:: + + sage: c = M.alternating_form(3, 'c') + sage: c[0,1,2] = 4 + sage: c.display() + c = 4 e^0/\e^1/\e^2 + sage: latex(c.display()) + c = 4 e^0\wedge e^1\wedge e^2 + + Display of a vanishing alternating form:: + + sage: c[0,1,2] = 0 # the only independent component set to zero + sage: c.is_zero() + True + sage: c.display() + c = 0 + sage: latex(c.display()) + c = 0 + sage: c[0,1,2] = 4 # value restored for what follows + + Display in a basis which is not the default one:: + + sage: aut = M.automorphism(matrix=[[0,1,0], [0,0,-1], [1,0,0]], + ....: basis=e) + sage: f = e.new_basis(aut, 'f') + sage: a.display(f) + a = 4 f^0 + f^1 + 3 f^2 + sage: a.disp(f) # shortcut notation + a = 4 f^0 + f^1 + 3 f^2 + sage: b.display(f) + b = -2 f^0/\f^1 - f^0/\f^2 - 3 f^1/\f^2 + sage: c.display(f) + c = -4 f^0/\f^1/\f^2 + + The output format can be set via the argument ``output_formatter`` + passed at the module construction:: + + sage: N = FiniteRankFreeModule(QQ, 3, name='N', start_index=1, + ....: output_formatter=Rational.numerical_approx) + sage: e = N.basis('e') + sage: b = N.alternating_form(2, 'b') + sage: b[1,2], b[1,3], b[2,3] = 1/3, 5/2, 4 + sage: b.display() # default format (53 bits of precision) + b = 0.333333333333333 e^1/\e^2 + 2.50000000000000 e^1/\e^3 + + 4.00000000000000 e^2/\e^3 + + The output format is then controled by the argument ``format_spec`` of + the method :meth:`display`:: + + sage: b.display(format_spec=10) # 10 bits of precision + b = 0.33 e^1/\e^2 + 2.5 e^1/\e^3 + 4.0 e^2/\e^3 + + """ + from sage.misc.latex import latex + from sage.tensor.modules.format_utilities import is_atomic, \ + FormattedExpansion + if basis is None: + basis = self._fmodule._def_basis + cobasis = basis.dual_basis() + comp = self.comp(basis) + terms_txt = [] + terms_latex = [] + for ind in comp.non_redundant_index_generator(): + ind_arg = ind + (format_spec,) + coef = comp[ind_arg] + if coef != 0: + bases_txt = [] + bases_latex = [] + for k in range(self._tensor_rank): + bases_txt.append(cobasis[ind[k]]._name) + bases_latex.append(latex(cobasis[ind[k]])) + basis_term_txt = "/\\".join(bases_txt) + basis_term_latex = r"\wedge ".join(bases_latex) + coef_txt = repr(coef) + if coef_txt == "1": + terms_txt.append(basis_term_txt) + terms_latex.append(basis_term_latex) + elif coef_txt == "-1": + terms_txt.append("-" + basis_term_txt) + terms_latex.append("-" + basis_term_latex) + else: + coef_latex = latex(coef) + if is_atomic(coef_txt): + terms_txt.append(coef_txt + " " + basis_term_txt) + else: + terms_txt.append("(" + coef_txt + ") " + + basis_term_txt) + if is_atomic(coef_latex): + terms_latex.append(coef_latex + basis_term_latex) + else: + terms_latex.append(r"\left(" + coef_latex + \ + r"\right)" + basis_term_latex) + if not terms_txt: + expansion_txt = "0" + else: + expansion_txt = terms_txt[0] + for term in terms_txt[1:]: + if term[0] == "-": + expansion_txt += " - " + term[1:] + else: + expansion_txt += " + " + term + if not terms_latex: + expansion_latex = "0" + else: + expansion_latex = terms_latex[0] + for term in terms_latex[1:]: + if term[0] == "-": + expansion_latex += term + else: + expansion_latex += "+" + term + if self._name is None: + resu_txt = expansion_txt + else: + resu_txt = self._name + " = " + expansion_txt + if self._latex_name is None: + resu_latex = expansion_latex + else: + resu_latex = latex(self) + " = " + expansion_latex + return FormattedExpansion(resu_txt, resu_latex) + + disp = display + + + def wedge(self, other): + r""" + Exterior product of ``self`` with the alternating form ``other``. + + INPUT: + + - ``other`` -- an alternating form + + OUTPUT: + + - instance of :class:`FreeModuleAltForm` representing the exterior + product ``self/\other`` + + EXAMPLES: + + Exterior product of two linear forms:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: a = M.linear_form('A') + sage: a[:] = [1,-3,4] + sage: b = M.linear_form('B') + sage: b[:] = [2,-1,2] + sage: c = a.wedge(b) ; c + Alternating form A/\B of degree 2 on the Rank-3 free module M + over the Integer Ring + sage: c.display() + A/\B = 5 e^0/\e^1 - 6 e^0/\e^2 - 2 e^1/\e^2 + sage: latex(c) + A\wedge B + sage: latex(c.display()) + A\wedge B = 5 e^0\wedge e^1 -6 e^0\wedge e^2 -2 e^1\wedge e^2 + + Test of the computation:: + + sage: a.wedge(b) == a*b - b*a + True + + Exterior product of a linear form and an alternating form of degree 2:: + + sage: d = M.linear_form('D') + sage: d[:] = [-1,2,4] + sage: s = d.wedge(c) ; s + Alternating form D/\A/\B of degree 3 on the Rank-3 free module M + over the Integer Ring + sage: s.display() + D/\A/\B = 34 e^0/\e^1/\e^2 + + Test of the computation:: + + sage: s[0,1,2] == d[0]*c[1,2] + d[1]*c[2,0] + d[2]*c[0,1] + True + + Let us check that the exterior product is associative:: + + sage: d.wedge(a.wedge(b)) == (d.wedge(a)).wedge(b) + True + + and that it is graded anticommutative:: + + sage: a.wedge(b) == - b.wedge(a) + True + sage: d.wedge(c) == c.wedge(d) + True + + """ + from format_utilities import is_atomic + if not isinstance(other, FreeModuleAltForm): + raise TypeError("the second argument for the exterior product " + + "must be an alternating form") + if other._tensor_rank == 0: + return other*self + if self._tensor_rank == 0: + return self*other + fmodule = self._fmodule + basis = self.common_basis(other) + if basis is None: + raise ValueError("no common basis for the exterior product") + rank_r = self._tensor_rank + other._tensor_rank + cmp_s = self._components[basis] + cmp_o = other._components[basis] + cmp_r = CompFullyAntiSym(fmodule._ring, basis, rank_r, + start_index=fmodule._sindex, + output_formatter=fmodule._output_formatter) + for ind_s, val_s in cmp_s._comp.iteritems(): + for ind_o, val_o in cmp_o._comp.iteritems(): + ind_r = ind_s + ind_o + if len(ind_r) == len(set(ind_r)): # all indices are different + cmp_r[[ind_r]] += val_s * val_o + result = fmodule.alternating_form(rank_r) + result._components[basis] = cmp_r + if self._name is not None and other._name is not None: + sname = self._name + oname = other._name + if not is_atomic(sname): + sname = '(' + sname + ')' + if not is_atomic(oname): + oname = '(' + oname + ')' + result._name = sname + '/\\' + oname + if self._latex_name is not None and other._latex_name is not None: + slname = self._latex_name + olname = other._latex_name + if not is_atomic(slname): + slname = '(' + slname + ')' + if not is_atomic(olname): + olname = '(' + olname + ')' + result._latex_name = slname + r'\wedge ' + olname + return result diff --git a/src/sage/tensor/modules/free_module_automorphism.py b/src/sage/tensor/modules/free_module_automorphism.py new file mode 100644 index 00000000000..ca2ae787d10 --- /dev/null +++ b/src/sage/tensor/modules/free_module_automorphism.py @@ -0,0 +1,1274 @@ +r""" +Free module automorphisms + +Given a free module `M` of finite rank over a commutative ring `R`, an +*automorphism* of `M` is a map + +.. MATH:: + + \phi:\ M \longrightarrow M + +that is linear (i.e. is a module homomorphism) and bijective. + +Automorphisms of a free module of finite rank are implemented via the class +:class:`FreeModuleAutomorphism`. + +AUTHORS: + +- Eric Gourgoulhon (2015): initial version + +REFERENCES: + +- Chaps. 15, 24 of R. Godement: *Algebra*, Hermann (Paris) / Houghton Mifflin + (Boston) (1968) + +""" +#****************************************************************************** +# Copyright (C) 2015 Eric Gourgoulhon +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# http://www.gnu.org/licenses/ +#****************************************************************************** + +from sage.structure.element import MultiplicativeGroupElement +from sage.tensor.modules.free_module_tensor import FreeModuleTensor + +class FreeModuleAutomorphism(FreeModuleTensor, MultiplicativeGroupElement): + r""" + Automorphism of a free module of finite rank over a commutative ring. + + This is a Sage *element* class, the corresponding *parent* class being + :class:`~sage.tensor.modules.free_module_linear_group.FreeModuleLinearGroup`. + + This class inherits from the classes + :class:`~sage.tensor.modules.free_module_tensor.FreeModuleTensor` + and + :class:`~sage.structure.element.MultiplicativeGroupElement`. + + INPUT: + + - ``fmodule`` -- free module `M` of finite rank over a commutative ring + `R`, as an instance of + :class:`~sage.tensor.modules.finite_rank_free_module.FiniteRankFreeModule` + - ``name`` -- (default: ``None``) name given to the automorphism + - ``latex_name`` -- (default: ``None``) LaTeX symbol to denote the + automorphism; if none is provided, the LaTeX symbol is set to ``name`` + - ``is_identity`` -- (default: ``False``) determines whether the + constructed object is the identity automorphism, i.e. the identity map + of `M` considered as an automorphism (the identity element of the + general linear group) + + EXAMPLES: + + Automorphism of a rank-2 free module over `\ZZ`:: + + sage: M = FiniteRankFreeModule(ZZ, 2, name='M', start_index=1) + sage: a = M.automorphism(name='a', latex_name=r'\alpha') ; a + Automorphism a of the Rank-2 free module M over the Integer Ring + sage: a.parent() + General linear group of the Rank-2 free module M over the Integer Ring + sage: a.parent() is M.general_linear_group() + True + sage: latex(a) + \alpha + + Setting the components of ``a`` w.r.t. a basis of module ``M``:: + + sage: e = M.basis('e') ; e + Basis (e_1,e_2) on the Rank-2 free module M over the Integer Ring + sage: a[:] = [[1,2],[1,3]] + sage: a.matrix(e) + [1 2] + [1 3] + sage: a(e[1]).display() + a(e_1) = e_1 + e_2 + sage: a(e[2]).display() + a(e_2) = 2 e_1 + 3 e_2 + + Actually, the components w.r.t. a given basis can be specified at the + construction of the object:: + + sage: a = M.automorphism(matrix=[[1,2],[1,3]], basis=e, name='a', + ....: latex_name=r'\alpha') ; a + Automorphism a of the Rank-2 free module M over the Integer Ring + sage: a.matrix(e) + [1 2] + [1 3] + + Since e is the module's default basis, it can be omitted in the argument + list:: + + sage: a == M.automorphism(matrix=[[1,2],[1,3]], name='a', + ....: latex_name=r'\alpha') + True + + The matrix of the automorphism can be obtained in any basis:: + + sage: f = M.basis('f', from_family=(3*e[1]+4*e[2], 5*e[1]+7*e[2])) ; f + Basis (f_1,f_2) on the Rank-2 free module M over the Integer Ring + sage: a.matrix(f) + [2 3] + [1 2] + + Automorphisms are tensors of type `(1,1)`:: + + sage: a.tensor_type() + (1, 1) + sage: a.tensor_rank() + 2 + + In particular, they can be displayed as such:: + + sage: a.display(e) + a = e_1*e^1 + 2 e_1*e^2 + e_2*e^1 + 3 e_2*e^2 + sage: a.display(f) + a = 2 f_1*f^1 + 3 f_1*f^2 + f_2*f^1 + 2 f_2*f^2 + + The automorphism acting on a module element:: + + sage: v = M([-2,3], name='v') ; v + Element v of the Rank-2 free module M over the Integer Ring + sage: a(v) + Element a(v) of the Rank-2 free module M over the Integer Ring + sage: a(v).display() + a(v) = 4 e_1 + 7 e_2 + + A second automorphism of the module ``M``:: + + sage: b = M.automorphism([[0,1],[-1,0]], name='b') ; b + Automorphism b of the Rank-2 free module M over the Integer Ring + sage: b.matrix(e) + [ 0 1] + [-1 0] + sage: b(e[1]).display() + b(e_1) = -e_2 + sage: b(e[2]).display() + b(e_2) = e_1 + + The composition of automorphisms is performed via the multiplication + operator:: + + sage: s = a*b ; s + Automorphism of the Rank-2 free module M over the Integer Ring + sage: s(v) == a(b(v)) + True + sage: s.matrix(f) + [ 11 19] + [ -7 -12] + sage: s.matrix(f) == a.matrix(f) * b.matrix(f) + True + + It is not commutative:: + + sage: a*b != b*a + True + + In other words, the parent of ``a`` and ``b``, i.e. the group + `\mathrm{GL}(M)`, is not abelian:: + + sage: M.general_linear_group() in CommutativeAdditiveGroups() + False + + The neutral element for the composition law is the module identity map:: + + sage: id = M.identity_map() ; id + Identity map of the Rank-2 free module M over the Integer Ring + sage: id.parent() + General linear group of the Rank-2 free module M over the Integer Ring + sage: id(v) == v + True + sage: id.matrix(f) + [1 0] + [0 1] + sage: id*a == a + True + sage: a*id == a + True + + The inverse of an automorphism is obtained via the method :meth:`inverse`, + or the operator ~, or the exponent -1:: + + sage: a.inverse() + Automorphism a^(-1) of the Rank-2 free module M over the Integer Ring + sage: a.inverse() is ~a + True + sage: a.inverse() is a^(-1) + True + sage: (a^(-1)).matrix(e) + [ 3 -2] + [-1 1] + sage: a*a^(-1) == id + True + sage: a^(-1)*a == id + True + sage: a^(-1)*s == b + True + sage: (a^(-1))(a(v)) == v + True + + The module's changes of basis are stored as automorphisms:: + + sage: M.change_of_basis(e,f) + Automorphism of the Rank-2 free module M over the Integer Ring + sage: M.change_of_basis(e,f).parent() + General linear group of the Rank-2 free module M over the Integer Ring + sage: M.change_of_basis(e,f).matrix(e) + [3 5] + [4 7] + sage: M.change_of_basis(f,e) == M.change_of_basis(e,f).inverse() + True + + The opposite of an automorphism is still an automorphism:: + + sage: -a + Automorphism -a of the Rank-2 free module M over the Integer Ring + sage: (-a).parent() + General linear group of the Rank-2 free module M over the Integer Ring + sage: (-a).matrix(e) == - (a.matrix(e)) + True + + Adding two automorphisms results in a generic type-(1,1) tensor:: + + sage: s = a + b ; s + Type-(1,1) tensor a+b on the Rank-2 free module M over the Integer Ring + sage: s.parent() + Free module of type-(1,1) tensors on the Rank-2 free module M over the + Integer Ring + sage: a[:], b[:], s[:] + ( + [1 2] [ 0 1] [1 3] + [1 3], [-1 0], [0 3] + ) + + To get the result as an endomorphism, one has to explicitely convert it via + the parent of endormophisms, `\mathrm{End}(M)`:: + + sage: s = End(M)(a+b) ; s + Generic endomorphism of Rank-2 free module M over the Integer Ring + sage: s(v) == a(v) + b(v) + True + sage: s.matrix(e) == a.matrix(e) + b.matrix(e) + True + sage: s.matrix(f) == a.matrix(f) + b.matrix(f) + True + + """ + def __init__(self, fmodule, name=None, latex_name=None, is_identity=False): + r""" + TESTS:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: from sage.tensor.modules.free_module_automorphism import FreeModuleAutomorphism + sage: a = FreeModuleAutomorphism(M, name='a') + sage: a[e,:] = [[-1,0,0],[0,1,2],[0,1,3]] + sage: TestSuite(a).run(skip="_test_category") # see below + + In the above test suite, _test_category fails because a is not an + instance of a.parent().category().element_class. Actually automorphism + must be constructed via FreeModuleLinearGroup.element_class and + not by a direct call to FreeModuleAutomorphism:: + + sage: a = M.general_linear_group().element_class(M, name='a') + sage: a[e,:] = [[-1,0,0],[0,1,2],[0,1,3]] + sage: TestSuite(a).run() + + Test suite on the identity map:: + + sage: id = M.general_linear_group().one() + sage: TestSuite(id).run() + + Test suite on the automorphism obtained as GL.an_element():: + + sage: b = M.general_linear_group().an_element() + sage: TestSuite(b).run() + + """ + if is_identity: + if name is None: + name = 'Id' + if latex_name is None: + if name == 'Id': + latex_name = r'\mathrm{Id}' + else: + latex_name = name + FreeModuleTensor.__init__(self, fmodule, (1,1), name=name, + latex_name=latex_name, + parent=fmodule.general_linear_group()) + # MultiplicativeGroupElement attributes: + # - none + # Local attributes: + self._is_identity = is_identity + self._inverse = None # inverse automorphism not set yet + self._matrices = {} + + #### SageObject methods #### + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(QQ, 3, name='M') + sage: M.automorphism() + Automorphism of the 3-dimensional vector space M over the Rational Field + sage: M.automorphism(name='a') + Automorphism a of the 3-dimensional vector space M over the Rational Field + sage: M.identity_map() + Identity map of the 3-dimensional vector space M over the Rational Field + + """ + if self._is_identity: + description = "Identity map " + else: + description = "Automorphism " + if self._name is not None: + description += self._name + " " + description += "of the {}".format(self._fmodule) + return description + + #### End of SageObject methods #### + + #### FreeModuleTensor methods #### + + def _new_instance(self): + r""" + Create an instance of the same class as ``self``. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: a = M.automorphism(name='a') + sage: a._new_instance() + Automorphism of the Rank-3 free module M over the Integer Ring + sage: Id = M.identity_map() + sage: Id._new_instance() + Automorphism of the Rank-3 free module M over the Integer Ring + + """ + return self.__class__(self._fmodule) + + def _del_derived(self): + r""" + Delete the derived quantities. + + EXAMPLE:: + + sage: M = FiniteRankFreeModule(QQ, 3, name='M') + sage: e = M.basis('e') + sage: a = M.automorphism(name='a') + sage: a[e,:] = [[1,0,-1], [0,3,0], [0,0,2]] + sage: b = a.inverse() + sage: a._inverse + Automorphism a^(-1) of the 3-dimensional vector space M over the + Rational Field + sage: a._del_derived() + sage: a._inverse # has been reset to None + + """ + # First delete the derived quantities pertaining to FreeModuleTensor: + FreeModuleTensor._del_derived(self) + # Then reset the inverse automorphism to None: + if self._inverse is not None: + self._inverse._inverse = None # (it was set to self) + self._inverse = None + # and delete the matrices: + self._matrices.clear() + + def _new_comp(self, basis): + r""" + Create some (uninitialized) components of ``self`` in a given basis. + + INPUT: + + - ``basis`` -- basis of the free module on which ``self`` is defined + + OUTPUT: + + - an instance of :class:`~sage.tensor.modules.comp.Components` or, + if ``self`` is the identity, of the subclass + :class:`~sage.tensor.modules.comp.KroneckerDelta` + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: a = M.automorphism() + sage: a._new_comp(e) + 2-indices components w.r.t. Basis (e_0,e_1,e_2) on the Rank-3 free + module M over the Integer Ring + sage: id = M.identity_map() + sage: id._new_comp(e) + Kronecker delta of size 3x3 + sage: type(id._new_comp(e)) + + + """ + from comp import KroneckerDelta + if self._is_identity: + fmodule = self._fmodule + return KroneckerDelta(fmodule._ring, basis, + start_index=fmodule._sindex, + output_formatter=fmodule._output_formatter) + return FreeModuleTensor._new_comp(self, basis) + + + def components(self, basis=None, from_basis=None): + r""" + Return the components of ``self`` w.r.t to a given module basis. + + If the components are not known already, they are computed by the + tensor change-of-basis formula from components in another basis. + + INPUT: + + - ``basis`` -- (default: ``None``) basis in which the components are + required; if none is provided, the components are assumed to refer + to the module's default basis + - ``from_basis`` -- (default: ``None``) basis from which the + required components are computed, via the tensor change-of-basis + formula, if they are not known already in the basis ``basis``; + if none, a basis from which both the components and a change-of-basis + to ``basis`` are known is selected. + + OUTPUT: + + - components in the basis ``basis``, as an instance of the + class :class:`~sage.tensor.modules.comp.Components`, + or, for the identity automorphism, of the subclass + :class:`~sage.tensor.modules.comp.KroneckerDelta` + + EXAMPLES: + + Components of an automorphism on a rank-3 free `\ZZ`-module:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M', start_index=1) + sage: e = M.basis('e') + sage: a = M.automorphism([[-1,0,0],[0,1,2],[0,1,3]], name='a') + sage: a.components(e) + 2-indices components w.r.t. Basis (e_1,e_2,e_3) on the Rank-3 free + module M over the Integer Ring + sage: a.components(e)[:] + [-1 0 0] + [ 0 1 2] + [ 0 1 3] + + Since e is the module's default basis, it can be omitted:: + + sage: a.components() is a.components(e) + True + + A shortcut is ``a.comp()``:: + + sage: a.comp() is a.components() + True + sage: a.comp(e) is a.components() + True + + Components in another basis:: + + sage: f1 = -e[2] + sage: f2 = 4*e[1] + 3*e[3] + sage: f3 = 7*e[1] + 5*e[3] + sage: f = M.basis('f', from_family=(f1,f2,f3)) + sage: a.components(f) + 2-indices components w.r.t. Basis (f_1,f_2,f_3) on the Rank-3 free + module M over the Integer Ring + sage: a.components(f)[:] + [ 1 -6 -10] + [ -7 83 140] + [ 4 -48 -81] + + Some check of the above matrix:: + + sage: a(f[1]).display(f) + a(f_1) = f_1 - 7 f_2 + 4 f_3 + sage: a(f[2]).display(f) + a(f_2) = -6 f_1 + 83 f_2 - 48 f_3 + sage: a(f[3]).display(f) + a(f_3) = -10 f_1 + 140 f_2 - 81 f_3 + + Components of the identity map:: + + sage: id = M.identity_map() + sage: id.components(e) + Kronecker delta of size 3x3 + sage: id.components(e)[:] + [1 0 0] + [0 1 0] + [0 0 1] + sage: id.components(f) + Kronecker delta of size 3x3 + sage: id.components(f)[:] + [1 0 0] + [0 1 0] + [0 0 1] + + """ + if self._is_identity: + if basis is None: + basis = self._fmodule._def_basis + if basis not in self._components: + self._components[basis] = self._new_comp(basis) + return self._components[basis] + else: + return FreeModuleTensor.components(self, basis=basis, + from_basis=from_basis) + + comp = components + + def set_comp(self, basis=None): + r""" + Return the components of ``self`` w.r.t. a given module basis for + assignment. + + The components with respect to other bases are deleted, in order to + avoid any inconsistency. To keep them, use the method :meth:`add_comp` + instead. + + INPUT: + + - ``basis`` -- (default: ``None``) basis in which the components are + defined; if none is provided, the components are assumed to refer to + the module's default basis + + OUTPUT: + + - components in the given basis, as an instance of the + class :class:`~sage.tensor.modules.comp.Components`; if such + components did not exist previously, they are created. + + EXAMPLE: + + Setting the components of an automorphism of a rank-3 free + `\ZZ`-module:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: a = M.automorphism(name='a') + sage: a.set_comp(e) + 2-indices components w.r.t. Basis (e_0,e_1,e_2) on the Rank-3 free + module M over the Integer Ring + sage: a.set_comp(e)[:] = [[1,0,0],[0,1,2],[0,1,3]] + sage: a.matrix(e) + [1 0 0] + [0 1 2] + [0 1 3] + + Since ``e`` is the module's default basis, one has:: + + sage: a.set_comp() is a.set_comp(e) + True + + The method :meth:`set_comp` can be used to modify a single component:: + + sage: a.set_comp(e)[0,0] = -1 + sage: a.matrix(e) + [-1 0 0] + [ 0 1 2] + [ 0 1 3] + + A short cut to :meth:`set_comp` is the bracket operator, with the basis + as first argument:: + + sage: a[e,:] = [[1,0,0],[0,-1,2],[0,1,-3]] + sage: a.matrix(e) + [ 1 0 0] + [ 0 -1 2] + [ 0 1 -3] + sage: a[e,0,0] = -1 + sage: a.matrix(e) + [-1 0 0] + [ 0 -1 2] + [ 0 1 -3] + + The call to :meth:`set_comp` erases the components previously defined + in other bases; to keep them, use the method :meth:`add_comp` instead:: + + sage: f = M.basis('f', from_family=(-e[0], 3*e[1]+4*e[2], + ....: 5*e[1]+7*e[2])) ; f + Basis (f_0,f_1,f_2) on the Rank-3 free module M over the Integer + Ring + sage: a._components + {Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer + Ring: 2-indices components w.r.t. Basis (e_0,e_1,e_2) on the + Rank-3 free module M over the Integer Ring} + sage: a.set_comp(f)[:] = [[-1,0,0], [0,1,0], [0,0,-1]] + + The components w.r.t. basis ``e`` have been erased:: + + sage: a._components + {Basis (f_0,f_1,f_2) on the Rank-3 free module M over the Integer + Ring: 2-indices components w.r.t. Basis (f_0,f_1,f_2) on the + Rank-3 free module M over the Integer Ring} + + Of course, they can be computed from those in basis ``f`` by means of + a change-of-basis formula, via the method :meth:`comp` or + :meth:`matrix`:: + + sage: a.matrix(e) + [ -1 0 0] + [ 0 41 -30] + [ 0 56 -41] + + For the identity map, it is not permitted to set components:: + + sage: id = M.identity_map() + sage: id.set_comp(e) + Traceback (most recent call last): + ... + TypeError: the components of the identity map cannot be changed + + Indeed, the components are automatically set by a call to + :meth:`comp`:: + + sage: id.comp(e) + Kronecker delta of size 3x3 + sage: id.comp(f) + Kronecker delta of size 3x3 + + """ + if self._is_identity: + raise TypeError("the components of the identity map cannot be " + + "changed") + else: + return FreeModuleTensor.set_comp(self, basis=basis) + + def add_comp(self, basis=None): + r""" + + Return the components of ``self`` w.r.t. a given module basis for + assignment, keeping the components w.r.t. other bases. + + To delete the components w.r.t. other bases, use the method + :meth:`set_comp` instead. + + INPUT: + + - ``basis`` -- (default: ``None``) basis in which the components are + defined; if none is provided, the components are assumed to refer to + the module's default basis + + .. WARNING:: + + If the automorphism has already components in other bases, it + is the user's responsability to make sure that the components + to be added are consistent with them. + + OUTPUT: + + - components in the given basis, as an instance of the + class :class:`~sage.tensor.modules.comp.Components`; + if such components did not exist previously, they are created + + EXAMPLE: + + Adding components to an automorphism of a rank-3 free + `\ZZ`-module:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: a = M.automorphism(name='a') + sage: a[e,:] = [[1,0,0],[0,-1,2],[0,1,-3]] + sage: f = M.basis('f', from_family=(-e[0], 3*e[1]+4*e[2], + ....: 5*e[1]+7*e[2])) ; f + Basis (f_0,f_1,f_2) on the Rank-3 free module M over the Integer + Ring + sage: a.add_comp(f)[:] = [[1,0,0], [0, 80, 143], [0, -47, -84]] + + The components in basis ``e`` have been kept:: + + sage: a._components # random (dictionary output) + {Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer + Ring: 2-indices components w.r.t. Basis (e_0,e_1,e_2) on the + Rank-3 free module M over the Integer Ring, + Basis (f_0,f_1,f_2) on the Rank-3 free module M over the Integer + Ring: 2-indices components w.r.t. Basis (f_0,f_1,f_2) on the + Rank-3 free module M over the Integer Ring} + + For the identity map, it is not permitted to invoke :meth:`add_comp`:: + + sage: id = M.identity_map() + sage: id.add_comp(e) + Traceback (most recent call last): + ... + TypeError: the components of the identity map cannot be changed + + Indeed, the components are automatically set by a call to + :meth:`comp`:: + + sage: id.comp(e) + Kronecker delta of size 3x3 + sage: id.comp(f) + Kronecker delta of size 3x3 + + """ + if self._is_identity: + raise TypeError("the components of the identity map cannot be " + + "changed") + else: + return FreeModuleTensor.add_comp(self, basis=basis) + + def __call__(self, *arg): + r""" + Redefinition of :meth:`FreeModuleTensor.__call__` to allow for a single + argument (module element). + + EXAMPLES: + + Call with a single argument: return a module element:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M', start_index=1) + sage: e = M.basis('e') + sage: a = M.automorphism([[-1,0,0],[0,1,2],[0,1,3]], name='a') + sage: v = M([2,1,4], name='v') + sage: s = a.__call__(v) ; s + Element a(v) of the Rank-3 free module M over the Integer Ring + sage: s.display() + a(v) = -2 e_1 + 9 e_2 + 13 e_3 + sage: s == a(v) + True + sage: s == a.contract(v) + True + + Call with two arguments (:class:`FreeModuleTensor` behaviour): return a + scalar:: + + sage: b = M.linear_form(name='b') + sage: b[:] = 7, 0, 2 + sage: a.__call__(b,v) + 12 + sage: a(b,v) == a.__call__(b,v) + True + sage: a(b,v) == s(b) + True + + Identity map with a single argument: return a module element:: + + sage: id = M.identity_map() + sage: s = id.__call__(v) ; s + Element v of the Rank-3 free module M over the Integer Ring + sage: s == v + True + sage: s == id(v) + True + sage: s == id.contract(v) + True + + Identity map with two arguments (:class:`FreeModuleTensor` behaviour): + return a scalar:: + + sage: id.__call__(b,v) + 22 + sage: id(b,v) == id.__call__(b,v) + True + sage: id(b,v) == b(v) + True + + """ + from free_module_tensor import FiniteRankFreeModuleElement + if len(arg) > 1: + # The automorphism acting as a type-(1,1) tensor on a pair + # (linear form, module element), returning a scalar: + if self._is_identity: + if len(arg) != 2: + raise TypeError("wrong number of arguments") + linform = arg[0] + if linform._tensor_type != (0,1): + raise TypeError("the first argument must be a linear form") + vector = arg[1] + if not isinstance(vector, FiniteRankFreeModuleElement): + raise TypeError("the second argument must be a module" + + " element") + return linform(vector) + else: # self is not the identity automorphism: + return FreeModuleTensor.__call__(self, *arg) + # The automorphism acting as such, on a module element, returning a + # module element: + vector = arg[0] + if not isinstance(vector, FiniteRankFreeModuleElement): + raise TypeError("the argument must be an element of a free module") + if self._is_identity: + return vector + basis = self.common_basis(vector) + t = self._components[basis] + v = vector._components[basis] + fmodule = self._fmodule + result = vector._new_instance() + for i in fmodule.irange(): + res = 0 + for j in fmodule.irange(): + res += t[[i,j]]*v[[j]] + result.set_comp(basis)[i] = res + # Name of the output: + result._name = None + if self._name is not None and vector._name is not None: + result._name = self._name + "(" + vector._name + ")" + # LaTeX symbol for the output: + result._latex_name = None + if self._latex_name is not None and vector._latex_name is not None: + result._latex_name = self._latex_name + r"\left(" + \ + vector._latex_name + r"\right)" + return result + + #### End of FreeModuleTensor methods #### + + #### MultiplicativeGroupElement methods #### + + def __invert__(self): + r""" + Return the inverse automorphism. + + OUTPUT: + + - instance of :class:`FreeModuleAutomorphism` representing the + automorphism that is the inverse of ``self``. + + EXAMPLES: + + Inverse of an automorphism of a rank-3 free module:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: a = M.automorphism(name='a') + sage: a[e,:] = [[1,0,0],[0,-1,2],[0,1,-3]] + sage: a.inverse() + Automorphism a^(-1) of the Rank-3 free module M over the Integer + Ring + sage: a.inverse().parent() + General linear group of the Rank-3 free module M over the Integer + Ring + + Check that ``a.inverse()`` is indeed the inverse automorphism:: + + sage: a.inverse() * a + Identity map of the Rank-3 free module M over the Integer Ring + sage: a * a.inverse() + Identity map of the Rank-3 free module M over the Integer Ring + sage: a.inverse().inverse() == a + True + + Another check is:: + + sage: a.inverse().matrix(e) + [ 1 0 0] + [ 0 -3 -2] + [ 0 -1 -1] + sage: a.inverse().matrix(e) == (a.matrix(e))^(-1) + True + + The inverse is cached (as long as ``a`` is not modified):: + + sage: a.inverse() is a.inverse() + True + + If ``a`` is modified, the inverse is automatically recomputed:: + + sage: a[0,0] = -1 + sage: a.matrix(e) + [-1 0 0] + [ 0 -1 2] + [ 0 1 -3] + sage: a.inverse().matrix(e) # compare with above + [-1 0 0] + [ 0 -3 -2] + [ 0 -1 -1] + + Shortcuts for :meth:`inverse` are the operator ``~`` and the exponent + ``-1``:: + + sage: ~a is a.inverse() + True + sage: a^(-1) is a.inverse() + True + + The inverse of the identity map is of course itself:: + + sage: id = M.identity_map() + sage: id.inverse() is id + True + + and we have:: + + sage: a*a^(-1) == id + True + sage: a^(-1)*a == id + True + + """ + from sage.matrix.constructor import matrix + from comp import Components + if self._is_identity: + return self + if self._inverse is None: + if self._name is None: + inv_name = None + else: + inv_name = self._name + '^(-1)' + if self._latex_name is None: + inv_latex_name = None + else: + inv_latex_name = self._latex_name + r'^{-1}' + fmodule = self._fmodule + si = fmodule._sindex + nsi = fmodule._rank + si + self._inverse = self.__class__(fmodule, inv_name, inv_latex_name) + for basis in self._components: + try: + mat = self.matrix(basis) + except (KeyError, ValueError): + continue + mat_inv = mat.inverse() + cinv = Components(fmodule._ring, basis, 2, start_index=si, + output_formatter=fmodule._output_formatter) + for i in range(si, nsi): + for j in range(si, nsi): + cinv[i, j] = mat_inv[i-si,j-si] + self._inverse._components[basis] = cinv + self._inverse._inverse = self + return self._inverse + + inverse = __invert__ + + def _mul_(self, other): + r""" + Automorphism composition. + + This implements the group law of GL(M), M being the module of ``self``. + + INPUT: + + - ``other`` -- an automorphism of the same module as ``self`` + + OUPUT: + + - the automorphism resulting from the composition of ``other`` and + ``self.`` + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 2, name='M') + sage: e = M.basis('e') + sage: a = M.automorphism([[1,2],[1,3]]) + sage: b = M.automorphism([[3,4],[5,7]]) + sage: c = a._mul_(b) ; c + Automorphism of the Rank-2 free module M over the Integer Ring + sage: c.matrix() + [13 18] + [18 25] + + TESTS:: + + sage: c.parent() is a.parent() + True + sage: c.matrix() == a.matrix() * b.matrix() + True + sage: c(e[0]) == a(b(e[0])) + True + sage: c(e[1]) == a(b(e[1])) + True + sage: a.inverse()._mul_(c) == b + True + sage: c._mul_(b.inverse()) == a + True + sage: id = M.identity_map() + sage: id._mul_(a) == a + True + sage: a._mul_(id) == a + True + sage: a._mul_(a.inverse()) == id + True + sage: a.inverse()._mul_(a) == id + True + + """ + # No need for consistency check since self and other are guaranted + # to have the same parent. In particular, they are defined on the same + # free module. + # + # Special cases: + if self._is_identity: + return other + if other._is_identity: + return self + if other is self._inverse or self is other._inverse: + return self._fmodule.identity_map() + # General case: + fmodule = self._fmodule + resu = self.__class__(fmodule) + basis = self.common_basis(other) + if basis is None: + raise ValueError("no common basis for the composition") + # The composition is performed as a tensor contraction of the last + # index of self (position=1) and the first index of other (position=0): + resu._components[basis] = self._components[basis].contract(1, + other._components[basis],0) + return resu + + #### End of MultiplicativeGroupElement methods #### + + def __mul__(self, other): + r""" + Redefinition of + :meth:`~sage.tensor.modules.free_module_tensor.FreeModuleTensor.__mul__` + so that * dispatches either to automorphism composition or to the + tensor product. + + EXAMPLES: + + Automorphism composition:: + + sage: M = FiniteRankFreeModule(ZZ, 2, name='M') + sage: e = M.basis('e') + sage: a = M.automorphism([[1,2],[1,3]]) + sage: b = M.automorphism([[3,4],[5,7]]) + sage: s = a*b ; s + Automorphism of the Rank-2 free module M over the Integer Ring + sage: s.matrix() + [13 18] + [18 25] + sage: s.matrix() == a.matrix() * b.matrix() + True + sage: s(e[0]) == a(b(e[0])) + True + sage: s(e[1]) == a(b(e[1])) + True + sage: s.display() + 13 e_0*e^0 + 18 e_0*e^1 + 18 e_1*e^0 + 25 e_1*e^1 + + Tensor product:: + + sage: c = M.tensor((1,1)) ; c + Type-(1,1) tensor on the Rank-2 free module M over the Integer Ring + sage: c[:] = [[3,4],[5,7]] + sage: c[:] == b[:] # c and b have the same components + True + sage: s = a*c ; s + Type-(2,2) tensor on the Rank-2 free module M over the Integer Ring + sage: s.display() + 3 e_0*e_0*e^0*e^0 + 4 e_0*e_0*e^0*e^1 + 6 e_0*e_0*e^1*e^0 + + 8 e_0*e_0*e^1*e^1 + 5 e_0*e_1*e^0*e^0 + 7 e_0*e_1*e^0*e^1 + + 10 e_0*e_1*e^1*e^0 + 14 e_0*e_1*e^1*e^1 + 3 e_1*e_0*e^0*e^0 + + 4 e_1*e_0*e^0*e^1 + 9 e_1*e_0*e^1*e^0 + 12 e_1*e_0*e^1*e^1 + + 5 e_1*e_1*e^0*e^0 + 7 e_1*e_1*e^0*e^1 + 15 e_1*e_1*e^1*e^0 + + 21 e_1*e_1*e^1*e^1 + + """ + if isinstance(other, FreeModuleAutomorphism): + return self._mul_(other) # general linear group law + else: + return FreeModuleTensor.__mul__(self, other) # tensor product + + def __imul__(self, other): + r""" + Redefinition of + :meth:`sage.structure.element.ModuleElement.__imul__` + + TESTS:: + + sage: M = FiniteRankFreeModule(ZZ, 2, name='M', start_index=1) + sage: e = M.basis('e') + sage: a = M.automorphism([[1,2],[1,3]], name='a') + sage: b = M.automorphism([[0,1],[-1,0]], name='b') + sage: mat_a0 = a.matrix(e) + sage: a.__imul__(b) + Automorphism of the Rank-2 free module M over the Integer Ring + sage: a *= b + sage: a.matrix(e) == mat_a0 * b.matrix(e) + True + + """ + return self.__mul__(other) + + def matrix(self, basis1=None, basis2=None): + r""" + Return the matrix of ``self`` w.r.t to a pair of bases. + + If the matrix is not known already, it is computed from the matrix in + another pair of bases by means of the change-of-basis formula. + + INPUT: + + - ``basis1`` -- (default: ``None``) basis of the free module on which + ``self`` is defined; if none is provided, the module's default basis + is assumed + - ``basis2`` -- (default: ``None``) basis of the free module on which + ``self`` is defined; if none is provided, ``basis2`` is set to + ``basis1`` + + OUTPUT: + + - the matrix representing representing the automorphism ``self`` w.r.t + to bases ``basis1`` and ``basis2``; more precisely, the columns of + this matrix are formed by the components w.r.t. ``basis2`` of + the images of the elements of ``basis1``. + + EXAMPLES: + + Matrices of an automorphism of a rank-3 free `\ZZ`-module:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M', start_index=1) + sage: e = M.basis('e') + sage: a = M.automorphism([[-1,0,0],[0,1,2],[0,1,3]], name='a') + sage: a.matrix(e) + [-1 0 0] + [ 0 1 2] + [ 0 1 3] + sage: a.matrix() + [-1 0 0] + [ 0 1 2] + [ 0 1 3] + sage: f = M.basis('f', from_family=(-e[2], 4*e[1]+3*e[3], 7*e[1]+5*e[3])) ; f + Basis (f_1,f_2,f_3) on the Rank-3 free module M over the Integer Ring + sage: a.matrix(f) + [ 1 -6 -10] + [ -7 83 140] + [ 4 -48 -81] + + Check of the above matrix:: + + sage: a(f[1]).display(f) + a(f_1) = f_1 - 7 f_2 + 4 f_3 + sage: a(f[2]).display(f) + a(f_2) = -6 f_1 + 83 f_2 - 48 f_3 + sage: a(f[3]).display(f) + a(f_3) = -10 f_1 + 140 f_2 - 81 f_3 + + Check of the change-of-basis formula:: + + sage: P = M.change_of_basis(e,f).matrix(e) + sage: a.matrix(f) == P^(-1) * a.matrix(e) * P + True + + Check that the matrix of the product of two automorphisms is the + product of their matrices:: + + sage: b = M.change_of_basis(e,f) ; b + Automorphism of the Rank-3 free module M over the Integer Ring + sage: b.matrix(e) + [ 0 4 7] + [-1 0 0] + [ 0 3 5] + sage: (a*b).matrix(e) == a.matrix(e) * b.matrix(e) + True + + Check that the matrix of the inverse automorphism is the inverse of the + automorphism's matrix:: + + sage: (~a).matrix(e) + [-1 0 0] + [ 0 3 -2] + [ 0 -1 1] + sage: (~a).matrix(e) == ~(a.matrix(e)) + True + + Matrices of the identity map:: + + sage: id = M.identity_map() + sage: id.matrix(e) + [1 0 0] + [0 1 0] + [0 0 1] + sage: id.matrix(f) + [1 0 0] + [0 1 0] + [0 0 1] + + """ + from sage.matrix.constructor import matrix + fmodule = self._fmodule + if basis1 is None: + basis1 = fmodule.default_basis() + elif basis1 not in fmodule.bases(): + raise TypeError("{} is not a basis on the {}".format(basis1, + fmodule)) + if basis2 is None: + basis2 = basis1 + elif basis2 not in fmodule.bases(): + raise TypeError("{} is not a basis on the {}".format(basis2, + fmodule)) + if (basis1, basis2) not in self._matrices: + if basis2 == basis1: + comp = self.components(basis1) + mat = [[comp[[i,j]] for j in fmodule.irange()] + for i in fmodule.irange()] + self._matrices[(basis1, basis1)] = matrix(mat) + else: + # 1/ determine the matrix w.r.t. basis1: + self.matrix(basis1) + # 2/ perform the change (basis1, basis1) --> (basis1, basis2): + raise NotImplementedError("basis1 != basis2 not implemented yet") + return self._matrices[(basis1, basis2)] + + def det(self): + r""" + Return the determinant of ``self``. + + OUTPUT: + + - element of the base ring of the module on which ``self`` is defined, + equal to the determinant of ``self``. + + EXAMPLES: + + Determinant of an automorphism on a `\ZZ`-module of rank 2:: + + sage: M = FiniteRankFreeModule(ZZ, 2, name='M') + sage: e = M.basis('e') + sage: a = M.automorphism([[4,7],[3,5]], name='a') + sage: a.matrix(e) + [4 7] + [3 5] + sage: a.det() + -1 + sage: det(a) + -1 + sage: ~a.det() # determinant of the inverse automorphism + -1 + sage: id = M.identity_map() + sage: id.det() + 1 + + """ + self.matrix() # forces the update of the matrix in the module's default + # basis, to make sure that the dictionary self._matrices + # is not empty + return self._matrices.values()[0].det() # pick a random value in the + # dictionary self._matrices + # and compute the determinant + + def trace(self): + r""" + Return the trace of ``self``. + + OUTPUT: + + - element of the base ring of the module on which ``self`` is defined, + equal to the trace of ``self``. + + EXAMPLES: + + Trace of an automorphism on a `\ZZ`-module of rank 2:: + + sage: M = FiniteRankFreeModule(ZZ, 2, name='M') + sage: e = M.basis('e') + sage: a = M.automorphism([[4,7],[3,5]], name='a') + sage: a.matrix(e) + [4 7] + [3 5] + sage: a.trace() + 9 + sage: id = M.identity_map() + sage: id.trace() + 2 + + """ + self.matrix() # forces the update of the matrix in the module's default + # basis, to make sure that the dictionary self._matrices + # is not empty + return self._matrices.values()[0].trace() # pick a random value in the + # dictionary self._matrices + # and compute the trace diff --git a/src/sage/tensor/modules/free_module_basis.py b/src/sage/tensor/modules/free_module_basis.py new file mode 100644 index 00000000000..26ddb6a4e33 --- /dev/null +++ b/src/sage/tensor/modules/free_module_basis.py @@ -0,0 +1,588 @@ +r""" +Free module bases + +The class :class:`FreeModuleBasis` implements bases on a free module `M` of +finite rank over a commutative ring, +while the class :class:`FreeModuleCoBasis` implements the dual bases (i.e. +bases of the dual module `M^*`). + +AUTHORS: + +- Eric Gourgoulhon, Michal Bejger (2014-2015): initial version + +REFERENCES: + +- Chap. 10 of R. Godement : *Algebra*, Hermann (Paris) / Houghton Mifflin + (Boston) (1968) +- Chap. 3 of S. Lang : *Algebra*, 3rd ed., Springer (New York) (2002) + +""" +#****************************************************************************** +# Copyright (C) 2015 Eric Gourgoulhon +# Copyright (C) 2015 Michal Bejger +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# http://www.gnu.org/licenses/ +#****************************************************************************** + +from sage.structure.sage_object import SageObject +from sage.structure.unique_representation import UniqueRepresentation + +class FreeModuleBasis(UniqueRepresentation, SageObject): + r""" + Basis of a free module over a commutative ring `R`. + + INPUT: + + - ``fmodule`` -- free module `M` (as an instance of + :class:`FiniteRankFreeModule`) + - ``symbol`` -- string; a letter (of a few letters) to denote a generic + element of the basis + - ``latex_symbol`` -- (default: ``None``) string; symbol to denote a + generic element of the basis; if ``None``, the value of ``symbol`` + is used + + EXAMPLES: + + A basis on a rank-3 free module over `\ZZ`:: + + sage: M0 = FiniteRankFreeModule(ZZ, 3, name='M_0') + sage: from sage.tensor.modules.free_module_basis import FreeModuleBasis + sage: e = FreeModuleBasis(M0, 'e') ; e + Basis (e_0,e_1,e_2) on the Rank-3 free module M_0 over the Integer Ring + + Instead of importing FreeModuleBasis in the global name space, it is + recommended to use the module's method + :meth:`~sage.tensor.modules.finite_rank_free_module.FiniteRankFreeModule.basis`:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') ; e + Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer Ring + + The individual elements constituting the basis are accessed via the + square bracket operator:: + + sage: e[0] + Element e_0 of the Rank-3 free module M over the Integer Ring + sage: e[0] in M + True + + The LaTeX symbol can be set explicitely, as the second argument of + :meth:`~sage.tensor.modules.finite_rank_free_module.FiniteRankFreeModule.basis`:: + + sage: latex(e) + \left(e_0,e_1,e_2\right) + sage: eps = M.basis('eps', r'\epsilon') ; eps + Basis (eps_0,eps_1,eps_2) on the Rank-3 free module M over the Integer + Ring + sage: latex(eps) + \left(\epsilon_0,\epsilon_1,\epsilon_2\right) + + The individual elements of the basis are labelled according the + parameter ``start_index`` provided at the free module construction:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M', start_index=1) + sage: e = M.basis('e') ; e + Basis (e_1,e_2,e_3) on the Rank-3 free module M over the Integer Ring + sage: e[1] + Element e_1 of the Rank-3 free module M over the Integer Ring + """ + @staticmethod + def __classcall_private__(cls, fmodule, symbol, latex_symbol=None): + """ + Normalize input to ensure a unique representation. + + TESTS:: + + sage: from sage.tensor.modules.free_module_basis import FreeModuleBasis + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = FreeModuleBasis(M, 'e', latex_symbol='e') + sage: e is FreeModuleBasis(M, 'e') + True + """ + if latex_symbol is None: + latex_symbol = symbol + return super(FreeModuleBasis, cls).__classcall__(cls, fmodule, symbol, latex_symbol) + + def __init__(self, fmodule, symbol, latex_symbol=None): + r""" + Initialize ``self``. + + TESTS:: + + sage: FiniteRankFreeModule._clear_cache_() # for doctests only + sage: from sage.tensor.modules.free_module_basis import FreeModuleBasis + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = FreeModuleBasis(M, 'e', latex_symbol=r'\epsilon') + sage: TestSuite(e).run() + + """ + self._fmodule = fmodule + if latex_symbol is None: + latex_symbol = symbol + self._name = "(" + \ + ",".join([symbol + "_" + str(i) for i in fmodule.irange()]) +")" + self._latex_name = r"\left(" + ",".join([latex_symbol + "_" + str(i) + for i in fmodule.irange()]) + r"\right)" + self._symbol = symbol + self._latex_symbol = latex_symbol + # The basis is added to the module list of bases + for other in fmodule._known_bases: + if symbol == other._symbol: + raise ValueError("the {} already exist on the {}".format(other, fmodule)) + fmodule._known_bases.append(self) + # The individual vectors: + vl = list() + for i in fmodule.irange(): + v_name = symbol + "_" + str(i) + v_symb = latex_symbol + "_" + str(i) + v = fmodule.element_class(fmodule, name=v_name, latex_name=v_symb) + for j in fmodule.irange(): + v.set_comp(self)[j] = fmodule._ring.zero() + v.set_comp(self)[i] = fmodule._ring.one() + vl.append(v) + self._vec = tuple(vl) + # The first defined basis is considered as the default one: + if fmodule._def_basis is None: + fmodule._def_basis = self + # Initialization of the components w.r.t the current basis of the zero + # elements of all tensor modules constructed up to now (including the + # base module itself, since it is considered as a type-(1,0) tensor + # module) + for t in fmodule._tensor_modules.itervalues(): + t._zero_element._components[self] = t._zero_element._new_comp(self) + # (since new components are initialized to zero) + # Initialization of the components w.r.t the current basis of the zero + # elements of all exterior powers constructed up to now + for t in fmodule._dual_exterior_powers.itervalues(): + t._zero_element._components[self] = t._zero_element._new_comp(self) + # (since new components are initialized to zero) + # The dual basis: + self._dual_basis = self._init_dual_basis() + + + ###### Methods to be redefined by derived classes of FreeModuleBasis ###### + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: e + Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer Ring + sage: M1 = FiniteRankFreeModule(ZZ, 3, name='M', start_index=1) + sage: e1 = M1.basis('e') + sage: e1 + Basis (e_1,e_2,e_3) on the Rank-3 free module M over the Integer Ring + + """ + return "Basis {} on the {}".format(self._name, self._fmodule) + + + def _init_dual_basis(self): + r""" + Construct the basis dual to ``self``. + + OUTPUT: + + - instance of :class:`FreeModuleCoBasis` representing the dual of + ``self`` + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: e._init_dual_basis() + Dual basis (e^0,e^1,e^2) on the Rank-3 free module M over the Integer Ring + + """ + return FreeModuleCoBasis(self, self._symbol, + latex_symbol=self._latex_symbol) + + def _new_instance(self, symbol, latex_symbol=None): + r""" + Construct a new basis on the same module as ``self``. + + INPUT: + + - ``symbol`` -- string; a letter (of a few letters) to denote a + generic element of the basis + - ``latex_symbol`` -- (default: ``None``) string; symbol to denote a + generic element of the basis; if ``None``, the value of ``symbol`` + is used + + OUTPUT: + + - instance of :class:`FreeModuleBasis` + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: e._new_instance('f') + Basis (f_0,f_1,f_2) on the Rank-3 free module M over the Integer Ring + + """ + return FreeModuleBasis(self._fmodule, symbol, latex_symbol=latex_symbol) + + ###### End of methods to be redefined by derived classes ###### + + def dual_basis(self): + r""" + Return the basis dual to ``self``. + + OUTPUT: + + - instance of :class:`FreeModuleCoBasis` representing the dual of + ``self`` + + EXAMPLES: + + Dual basis on a rank-3 free module:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M', start_index=1) + sage: e = M.basis('e') ; e + Basis (e_1,e_2,e_3) on the Rank-3 free module M over the Integer Ring + sage: f = e.dual_basis() ; f + Dual basis (e^1,e^2,e^3) on the Rank-3 free module M over the Integer Ring + + Let us check that the elements of f are elements of the dual of M:: + + sage: f[1] in M.dual() + True + sage: f[1] + Linear form e^1 on the Rank-3 free module M over the Integer Ring + + and that f is indeed the dual of e:: + + sage: f[1](e[1]), f[1](e[2]), f[1](e[3]) + (1, 0, 0) + sage: f[2](e[1]), f[2](e[2]), f[2](e[3]) + (0, 1, 0) + sage: f[3](e[1]), f[3](e[2]), f[3](e[3]) + (0, 0, 1) + + """ + return self._dual_basis + + def _latex_(self): + r""" + LaTeX representation of the object. + + EXAMPLES:: + + sage: FiniteRankFreeModule._clear_cache_() # for doctests only + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: e._latex_() + '\\left(e_0,e_1,e_2\\right)' + sage: latex(e) + \left(e_0,e_1,e_2\right) + sage: f = M.basis('eps', latex_symbol=r'\epsilon') + sage: f._latex_() + '\\left(\\epsilon_0,\\epsilon_1,\\epsilon_2\\right)' + sage: latex(f) + \left(\epsilon_0,\epsilon_1,\epsilon_2\right) + + """ + return self._latex_name + + def __getitem__(self, index): + r""" + Return the basis element corresponding to the given index ``index``. + + INPUT: + + - ``index`` -- the index of the basis element + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: e.__getitem__(0) + Element e_0 of the Rank-3 free module M over the Integer Ring + sage: e.__getitem__(1) + Element e_1 of the Rank-3 free module M over the Integer Ring + sage: e.__getitem__(2) + Element e_2 of the Rank-3 free module M over the Integer Ring + sage: e[1] is e.__getitem__(1) + True + sage: e[1].parent() is M + True + sage: M1 = FiniteRankFreeModule(ZZ, 3, name='M', start_index=1) + sage: e1 = M1.basis('e') + sage: e1.__getitem__(1) + Element e_1 of the Rank-3 free module M over the Integer Ring + sage: e1.__getitem__(2) + Element e_2 of the Rank-3 free module M over the Integer Ring + sage: e1.__getitem__(3) + Element e_3 of the Rank-3 free module M over the Integer Ring + + """ + n = self._fmodule._rank + si = self._fmodule._sindex + i = index - si + if i < 0 or i > n-1: + raise ValueError("Index out of range: " + + str(i+si) + " not in [" + str(si) + "," + + str(n-1+si) + "]") + return self._vec[i] + + def __len__(self): + r""" + Return the basis length, i.e. the rank of the free module. + + NB: the method ``__len__()`` is required for the basis to act as a + "frame" in the class :class:`~sage.tensor.modules.comp.Components`. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: e.__len__() + 3 + sage: len(e) + 3 + + """ + return self._fmodule._rank + + def new_basis(self, change_of_basis, symbol, latex_symbol=None): + r""" + Define a new module basis from ``self``. + + The new basis is defined by means of a module automorphism. + + INPUT: + + - ``change_of_basis`` -- instance of + :class:`~sage.tensor.modules.free_module_automorphism.FreeModuleAutomorphism` + describing the automorphism `P` that relates the current basis + `(e_i)` (described by ``self``) to the new basis `(n_i)` according + to `n_i = P(e_i)` + - ``symbol`` -- string; a letter (of a few letters) to denote a + generic element of the basis + - ``latex_symbol`` -- (default: ``None``) string; symbol to denote a + generic element of the basis; if ``None``, the value of ``symbol`` + is used + + OUTPUT: + + - the new basis `(n_i)`, as an instance of :class:`FreeModuleBasis` + + EXAMPLES: + + Change of basis on a vector space of dimension 2:: + + sage: M = FiniteRankFreeModule(QQ, 2, name='M', start_index=1) + sage: e = M.basis('e') + sage: a = M.automorphism() + sage: a[:] = [[1, 2], [-1, 3]] + sage: f = e.new_basis(a, 'f') ; f + Basis (f_1,f_2) on the 2-dimensional vector space M over the + Rational Field + sage: f[1].display() + f_1 = e_1 - e_2 + sage: f[2].display() + f_2 = 2 e_1 + 3 e_2 + sage: e[1].display(f) + e_1 = 3/5 f_1 + 1/5 f_2 + sage: e[2].display(f) + e_2 = -2/5 f_1 + 1/5 f_2 + + """ + from free_module_automorphism import FreeModuleAutomorphism + if not isinstance(change_of_basis, FreeModuleAutomorphism): + raise TypeError("the argument change_of_basis must be some " + + "instance of FreeModuleAutomorphism") + fmodule = self._fmodule + # self._new_instance used instead of FreeModuleBasis for a correct + # construction in case of derived classes: + the_new_basis = self._new_instance(symbol, latex_symbol=latex_symbol) + transf = change_of_basis.copy() + inv_transf = change_of_basis.inverse().copy() + si = fmodule._sindex + # Components of the new basis vectors in the old basis: + for i in fmodule.irange(): + for j in fmodule.irange(): + the_new_basis._vec[i-si].add_comp(self)[[j]] = \ + transf.comp(self)[[j,i]] + # Components of the new dual-basis elements in the old dual basis: + for i in fmodule.irange(): + for j in fmodule.irange(): + the_new_basis._dual_basis._form[i-si].add_comp(self)[[j]] = \ + inv_transf.comp(self)[[i,j]] + # The components of the transformation and its inverse are the same in + # the two bases: + for i in fmodule.irange(): + for j in fmodule.irange(): + transf.add_comp(the_new_basis)[[i,j]] = transf.comp(self)[[i,j]] + inv_transf.add_comp(the_new_basis)[[i,j]] = \ + inv_transf.comp(self)[[i,j]] + # Components of the old basis vectors in the new basis: + for i in fmodule.irange(): + for j in fmodule.irange(): + self._vec[i-si].add_comp(the_new_basis)[[j]] = \ + inv_transf.comp(self)[[j,i]] + # Components of the old dual-basis elements in the new cobasis: + for i in fmodule.irange(): + for j in fmodule.irange(): + self._dual_basis._form[i-si].add_comp(the_new_basis)[[j]] = \ + transf.comp(self)[[i,j]] + # The automorphism and its inverse are added to the module's dictionary + # of changes of bases: + fmodule._basis_changes[(self, the_new_basis)] = transf + fmodule._basis_changes[(the_new_basis, self)] = inv_transf + # + return the_new_basis + + +#****************************************************************************** + +class FreeModuleCoBasis(UniqueRepresentation, SageObject): + r""" + Dual basis of a free module over a commutative ring. + + INPUT: + + - ``basis`` -- basis of a free module `M` of which ``self`` is the dual + (must be an instance of :class:`FreeModuleBasis`) + - ``symbol`` -- a letter (of a few letters) to denote a generic element of + the cobasis + - ``latex_symbol`` -- (default: ``None``) symbol to denote a generic + element of the cobasis; if ``None``, the value of ``symbol`` is used + + EXAMPLES: + + Dual basis on a rank-3 free module:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M', start_index=1) + sage: e = M.basis('e') ; e + Basis (e_1,e_2,e_3) on the Rank-3 free module M over the Integer Ring + sage: from sage.tensor.modules.free_module_basis import FreeModuleCoBasis + sage: f = FreeModuleCoBasis(e, 'f') ; f + Dual basis (f^1,f^2,f^3) on the Rank-3 free module M over the Integer Ring + + Let us check that the elements of ``f`` are in the dual of ``M``:: + + sage: f[1] in M.dual() + True + sage: f[1] + Linear form f^1 on the Rank-3 free module M over the Integer Ring + + and that ``f`` is indeed the dual of ``e``:: + + sage: f[1](e[1]), f[1](e[2]), f[1](e[3]) + (1, 0, 0) + sage: f[2](e[1]), f[2](e[2]), f[2](e[3]) + (0, 1, 0) + sage: f[3](e[1]), f[3](e[2]), f[3](e[3]) + (0, 0, 1) + + """ + def __init__(self, basis, symbol, latex_symbol=None): + r""" + TEST:: + + sage: from sage.tensor.modules.free_module_basis import FreeModuleCoBasis + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: f = FreeModuleCoBasis(e, 'f') + sage: TestSuite(f).run() + + """ + self._basis = basis + self._fmodule = basis._fmodule + self._name = "(" + \ + ",".join([symbol + "^" + str(i) for i in self._fmodule.irange()]) +")" + if latex_symbol is None: + latex_symbol = symbol + self._latex_name = r"\left(" + \ + ",".join([latex_symbol + "^" + str(i) + for i in self._fmodule.irange()]) + r"\right)" + # The individual linear forms: + vl = list() + for i in self._fmodule.irange(): + v_name = symbol + "^" + str(i) + v_symb = latex_symbol + "^" + str(i) + v = self._fmodule.linear_form(name=v_name, latex_name=v_symb) + for j in self._fmodule.irange(): + v.set_comp(basis)[j] = 0 + v.set_comp(basis)[i] = 1 + vl.append(v) + self._form = tuple(vl) + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: f = e.dual_basis() + sage: f + Dual basis (e^0,e^1,e^2) on the + Rank-3 free module M over the Integer Ring + + """ + return "Dual basis {} on the {}".format(self._name, self._fmodule) + + def _latex_(self): + r""" + Return a LaTeX representation of ``self``. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: f = e.dual_basis() + sage: f._latex_() + '\\left(e^0,e^1,e^2\\right)' + + """ + return self._latex_name + + def __getitem__(self, index): + r""" + Return the basis linear form corresponding to a given index. + + INPUT: + + - ``index`` -- the index of the linear form + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: f = e.dual_basis() + sage: f[0] + Linear form e^0 on the Rank-3 free module M over the Integer Ring + sage: f[1] + Linear form e^1 on the Rank-3 free module M over the Integer Ring + sage: f[2] + Linear form e^2 on the Rank-3 free module M over the Integer Ring + sage: f[1] is f.__getitem__(1) + True + sage: M1 = FiniteRankFreeModule(ZZ, 3, name='M', start_index=1) + sage: f1 = M1.basis('e').dual_basis() + sage: f1[1] + Linear form e^1 on the Rank-3 free module M over the Integer Ring + sage: f1[2] + Linear form e^2 on the Rank-3 free module M over the Integer Ring + sage: f1[3] + Linear form e^3 on the Rank-3 free module M over the Integer Ring + + """ + n = self._fmodule._rank + si = self._fmodule._sindex + i = index - si + if i < 0 or i > n-1: + raise IndexError("out of range: {} not in [{},{}]".format(i+si, + si, n-1+si)) + return self._form[i] diff --git a/src/sage/tensor/modules/free_module_homset.py b/src/sage/tensor/modules/free_module_homset.py new file mode 100644 index 00000000000..d9b45200761 --- /dev/null +++ b/src/sage/tensor/modules/free_module_homset.py @@ -0,0 +1,533 @@ +r""" +Sets of morphisms between free modules + +The class :class:`FreeModuleHomset` implements sets of homomorphisms between +two free modules of finite rank over the same commutative ring. + +AUTHORS: + +- Eric Gourgoulhon, Michal Bejger (2014-2015): initial version + +REFERENCES: + +- Chaps. 13, 14 of R. Godement : *Algebra*, Hermann (Paris) / Houghton Mifflin + (Boston) (1968) +- Chap. 3 of S. Lang : *Algebra*, 3rd ed., Springer (New York) (2002) + +""" +#****************************************************************************** +# Copyright (C) 2015 Eric Gourgoulhon +# Copyright (C) 2015 Michal Bejger +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# http://www.gnu.org/licenses/ +#****************************************************************************** + +from sage.categories.homset import Homset +from sage.tensor.modules.free_module_morphism import FiniteRankFreeModuleMorphism +from sage.tensor.modules.free_module_automorphism import FreeModuleAutomorphism +from sage.tensor.modules.free_module_tensor import FreeModuleTensor + +class FreeModuleHomset(Homset): + r""" + Set of homomorphisms between free modules of finite rank over a + commutative ring. + + Given two free modules `M` and `N` of respective ranks `m` and `n` over a + commutative ring `R`, the class :class:`FreeModuleHomset` implements the + set `\mathrm{Hom}(M,N)` of homomorphisms `M\rightarrow N`. + The set `\mathrm{Hom}(M,N)` is actually a free module of rank `mn` over + `R`, but this aspect is not taken into account here. + + This is a Sage *parent* class, whose *element* class is + :class:`~sage.tensor.modules.free_module_morphism.FiniteRankFreeModuleMorphism`. + + INPUT: + + - ``fmodule1`` -- free module `M` (domain of the homomorphisms), as an + instance of + :class:`~sage.tensor.modules.finite_rank_free_module.FiniteRankFreeModule` + - ``fmodule2`` -- free module `N` (codomain of the homomorphisms), as an + instance of + :class:`~sage.tensor.modules.finite_rank_free_module.FiniteRankFreeModule` + - ``name`` -- (default: ``None``) string; name given to the hom-set; if + none is provided, Hom(M,N) will be used + - ``latex_name`` -- (default: ``None``) string; LaTeX symbol to denote the + hom-set; if none is provided, `\mathrm{Hom}(M,N)` will be used + + EXAMPLES: + + Set of homomorphisms between two free modules over `\ZZ`:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: N = FiniteRankFreeModule(ZZ, 2, name='N') + sage: H = Hom(M,N) ; H + Set of Morphisms from Rank-3 free module M over the Integer Ring to + Rank-2 free module N over the Integer Ring in Category of modules + over Integer Ring + sage: type(H) + + sage: H.category() + Category of homsets of modules over Integer Ring + + Hom-sets are cached:: + + sage: H is Hom(M,N) + True + + The LaTeX formatting is:: + + sage: latex(H) + \mathrm{Hom}\left(M,N\right) + + As usual, the construction of an element is performed by the ``__call__`` + method; the argument can be the matrix representing the morphism in the + default bases of the two modules:: + + sage: e = M.basis('e') + sage: f = N.basis('f') + sage: phi = H([[-1,2,0], [5,1,2]]) ; phi + Generic morphism: + From: Rank-3 free module M over the Integer Ring + To: Rank-2 free module N over the Integer Ring + sage: phi.parent() is H + True + + An example of construction from a matrix w.r.t. bases that are not the + default ones:: + + sage: ep = M.basis('ep', latex_symbol=r"e'") + sage: fp = N.basis('fp', latex_symbol=r"f'") + sage: phi2 = H([[3,2,1], [1,2,3]], bases=(ep,fp)) ; phi2 + Generic morphism: + From: Rank-3 free module M over the Integer Ring + To: Rank-2 free module N over the Integer Ring + + The zero element:: + + sage: z = H.zero() ; z + Generic morphism: + From: Rank-3 free module M over the Integer Ring + To: Rank-2 free module N over the Integer Ring + sage: z.matrix(e,f) + [0 0 0] + [0 0 0] + + The test suite for H is passed:: + + sage: TestSuite(H).run() + + The set of homomorphisms `M\rightarrow M`, i.e. endomorphisms, is + obtained by the function ``End``:: + + sage: End(M) + Set of Morphisms from Rank-3 free module M over the Integer Ring + to Rank-3 free module M over the Integer Ring in Category of modules + over Integer Ring + + ``End(M)`` is actually identical to ``Hom(M,M)``:: + + sage: End(M) is Hom(M,M) + True + + The unit of the endomorphism ring is the identity map:: + + sage: End(M).one() + Identity endomorphism of Rank-3 free module M over the Integer Ring + + whose matrix in any basis is of course the identity matrix:: + + sage: End(M).one().matrix(e) + [1 0 0] + [0 1 0] + [0 0 1] + + There is a canonical identification between endomorphisms of `M` and + tensors of type (1,1) on `M`. Accordingly, coercion maps have been + implemented between `\mathrm{End}(M)` and `T^{(1,1)}(M)` (the module of + all type-(1,1) tensors on `M`, see + :class:`~sage.tensor.modules.tensor_free_module.TensorFreeModule`):: + + sage: T11 = M.tensor_module(1,1) ; T11 + Free module of type-(1,1) tensors on the Rank-3 free module M over + the Integer Ring + sage: End(M).has_coerce_map_from(T11) + True + sage: T11.has_coerce_map_from(End(M)) + True + + See :class:`~sage.tensor.modules.tensor_free_module.TensorFreeModule` for + examples of the above coercions. + + There is a coercion `\mathrm{GL}(M) \rightarrow \mathrm{End}(M)`, since + every automorphism is an endomorphism:: + + sage: GL = M.general_linear_group() ; GL + General linear group of the Rank-3 free module M over the Integer Ring + sage: End(M).has_coerce_map_from(GL) + True + + Of course, there is no coercion in the reverse direction, since only + bijective endomorphisms are automorphisms:: + + sage: GL.has_coerce_map_from(End(M)) + False + + The coercion `\mathrm{GL}(M) \rightarrow \mathrm{End}(M)` in action:: + + sage: a = GL.an_element() ; a + Automorphism of the Rank-3 free module M over the Integer Ring + sage: a.matrix(e) + [ 1 0 0] + [ 0 -1 0] + [ 0 0 1] + sage: ea = End(M)(a) ; ea + Generic endomorphism of Rank-3 free module M over the Integer Ring + sage: ea.matrix(e) + [ 1 0 0] + [ 0 -1 0] + [ 0 0 1] + + """ + + Element = FiniteRankFreeModuleMorphism + + def __init__(self, fmodule1, fmodule2, name=None, latex_name=None): + r""" + TESTS:: + + sage: from sage.tensor.modules.free_module_homset import FreeModuleHomset + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: N = FiniteRankFreeModule(ZZ, 2, name='N') + sage: FreeModuleHomset(M, N) + Set of Morphisms from Rank-3 free module M over the Integer Ring + to Rank-2 free module N over the Integer Ring in Category of + modules over Integer Ring + sage: H = FreeModuleHomset(M, N, name='L(M,N)', + ....: latex_name=r'\mathcal{L}(M,N)') + sage: latex(H) + \mathcal{L}(M,N) + + """ + from finite_rank_free_module import FiniteRankFreeModule + if not isinstance(fmodule1, FiniteRankFreeModule): + raise TypeError("fmodule1 = {} is not an ".format(fmodule1) + + "instance of FiniteRankFreeModule") + if not isinstance(fmodule2, FiniteRankFreeModule): + raise TypeError("fmodule2 = {} is not an ".format(fmodule2) + + "instance of FiniteRankFreeModule") + if fmodule1.base_ring() != fmodule2.base_ring(): + raise TypeError("the domain and codomain are not defined over " + + "the same ring") + Homset.__init__(self, fmodule1, fmodule2) + if name is None: + self._name = "Hom(" + fmodule1._name + "," + fmodule2._name + ")" + else: + self._name = name + if latex_name is None: + self._latex_name = \ + r"\mathrm{Hom}\left(" + fmodule1._latex_name + "," + \ + fmodule2._latex_name + r"\right)" + else: + self._latex_name = latex_name + self._one = None # to be set by self.one() if self is an endomorphism + # set (fmodule1 = fmodule2) + + def _latex_(self): + r""" + LaTeX representation of the object. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: N = FiniteRankFreeModule(ZZ, 2, name='N') + sage: H = Hom(M,N) + sage: H._latex_() + '\\mathrm{Hom}\\left(M,N\\right)' + sage: latex(H) # indirect doctest + \mathrm{Hom}\left(M,N\right) + + """ + if self._latex_name is None: + return r'\mbox{' + str(self) + r'}' + else: + return self._latex_name + + def __call__(self, *args, **kwds): + r""" + To bypass Homset.__call__, enforcing Parent.__call__ instead. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 2, name='M') + sage: N = FiniteRankFreeModule(ZZ, 3, name='N') + sage: H = Hom(M,N) + sage: e = M.basis('e') ; f = N.basis('f') + sage: a = H.__call__(0) ; a + Generic morphism: + From: Rank-2 free module M over the Integer Ring + To: Rank-3 free module N over the Integer Ring + sage: a.matrix(e,f) + [0 0] + [0 0] + [0 0] + sage: a == H.zero() + True + sage: a == H(0) + True + sage: a = H.__call__([[1,2],[3,4],[5,6]], bases=(e,f), name='a') ; a + Generic morphism: + From: Rank-2 free module M over the Integer Ring + To: Rank-3 free module N over the Integer Ring + sage: a.matrix(e,f) + [1 2] + [3 4] + [5 6] + sage: a == H([[1,2],[3,4],[5,6]], bases=(e,f)) + True + + """ + from sage.structure.parent import Parent + return Parent.__call__(self, *args, **kwds) + + #### Methods required for any Parent + + def _element_constructor_(self, matrix_rep, bases=None, name=None, + latex_name=None, is_identity=False): + r""" + Construct an element of ``self``, i.e. a homomorphism M --> N, where + M is the domain of ``self`` and N its codomain. + + INPUT: + + - ``matrix_rep`` -- matrix representation of the homomorphism with + respect to the bases ``basis1`` and ``basis2``; this entry can + actually be any material from which a matrix of size rank(N)*rank(M) + can be constructed + - ``bases`` -- (default: ``None``) pair (basis_M, basis_N) defining the + matrix representation, basis_M being a basis of module `M` and + basis_N a basis of module `N` ; if none is provided the pair formed + by the default bases of each module is assumed. + - ``name`` -- (default: ``None``) string; name given to the + homomorphism + - ``latex_name`` -- (default: ``None``)string; LaTeX symbol to denote + the homomorphism; if none is provided, ``name`` will be used. + - ``is_identity`` -- (default: ``False``) determines whether the + constructed object is the identity endomorphism; if set to ``True``, + then N must be M and the entry ``matrix_rep`` is not used. + + EXAMPLES: + + Construction of a homomorphism between two free `\ZZ`-modules:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: N = FiniteRankFreeModule(ZZ, 2, name='N') + sage: e = M.basis('e') ; f = N.basis('f') + sage: H = Hom(M,N) + sage: phi = H._element_constructor_([[2,-1,3], [1,0,-4]], bases=(e,f), + ....: name='phi', latex_name=r'\phi') + sage: phi + Generic morphism: + From: Rank-3 free module M over the Integer Ring + To: Rank-2 free module N over the Integer Ring + sage: phi.matrix(e,f) + [ 2 -1 3] + [ 1 0 -4] + sage: phi == H([[2,-1,3], [1,0,-4]], bases=(e,f), name='phi', + ....: latex_name=r'\phi') + True + + Construction of an endomorphism:: + + sage: EM = End(M) + sage: phi = EM._element_constructor_([[1,2,3],[4,5,6],[7,8,9]], + ....: name='phi', latex_name=r'\phi') + sage: phi + Generic endomorphism of Rank-3 free module M over the Integer Ring + sage: phi.matrix(e,e) + [1 2 3] + [4 5 6] + [7 8 9] + + Coercion of a type-(1,1) tensor to an endomorphism:: + + sage: a = M.tensor((1,1)) + sage: a[:] = [[1,2,3],[4,5,6],[7,8,9]] + sage: EM = End(M) + sage: phi_a = EM._element_constructor_(a) ; phi_a + Generic endomorphism of Rank-3 free module M over the Integer Ring + sage: phi_a.matrix(e,e) + [1 2 3] + [4 5 6] + [7 8 9] + sage: phi_a == phi + True + sage: phi_a1 = EM(a) ; phi_a1 # indirect doctest + Generic endomorphism of Rank-3 free module M over the Integer Ring + sage: phi_a1 == phi + True + + """ + if isinstance(matrix_rep, FreeModuleTensor): + # coercion of a type-(1,1) tensor to an endomorphism + # (this includes automorphisms, since the class + # FreeModuleAutomorphism inherits from FreeModuleTensor) + tensor = matrix_rep # for readability + if tensor.tensor_type() == (1,1) and \ + self.is_endomorphism_set() and \ + tensor.base_module() is self.domain(): + basis = tensor.pick_a_basis() + tcomp = tensor.comp(basis) + fmodule = tensor.base_module() + mat = [[ tcomp[[i,j]] for j in fmodule.irange()] \ + for i in fmodule.irange()] + if isinstance(tensor, FreeModuleAutomorphism): + is_identity = tensor._is_identity + else: + is_identity = False + resu = self.element_class(self, mat, bases=(basis,basis), + name=tensor._name, latex_name=tensor._latex_name, + is_identity=is_identity) + else: + raise TypeError("cannot coerce the {}".format(tensor) + + " to an element of {}".format(self)) + else: + # Standard construction: + resu = self.element_class(self, matrix_rep, bases=bases, name=name, + latex_name=latex_name, + is_identity=is_identity) + return resu + + def _an_element_(self): + r""" + Construct some (unamed) element. + + EXAMPLE:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: N = FiniteRankFreeModule(ZZ, 2, name='N') + sage: e = M.basis('e') ; f = N.basis('f') + sage: phi = Hom(M,N)._an_element_() ; phi + Generic morphism: + From: Rank-3 free module M over the Integer Ring + To: Rank-2 free module N over the Integer Ring + sage: phi.matrix(e,f) + [1 1 1] + [1 1 1] + sage: phi == Hom(M,N).an_element() + True + + """ + ring = self.base_ring() + m = self.domain().rank() + n = self.codomain().rank() + matrix_rep = [[ring.an_element() for i in range(m)] for j in range(n)] + return self.element_class(self, matrix_rep) + + def _coerce_map_from_(self, other): + r""" + Determine whether coercion to self exists from other parent. + + EXAMPLES: + + The module of type-(1,1) tensors coerces to ``self``, if the latter + is some endomorphism set:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: End(M)._coerce_map_from_(M.tensor_module(1,1)) + True + sage: End(M).has_coerce_map_from(M.tensor_module(1,1)) + True + sage: End(M)._coerce_map_from_(M.tensor_module(1,2)) + False + + The general linear group coerces to the endomorphism ring:: + + sage: End(M)._coerce_map_from_(M.general_linear_group()) + True + + """ + from sage.tensor.modules.tensor_free_module import TensorFreeModule + from sage.tensor.modules.free_module_linear_group import \ + FreeModuleLinearGroup + if isinstance(other, TensorFreeModule): + # Coercion of a type-(1,1) tensor to an endomorphism: + if other.tensor_type() == (1,1): + return self.is_endomorphism_set() and \ + other.base_module() is self.domain() + if isinstance(other, FreeModuleLinearGroup): + # Coercion of an automorphism to an endomorphism: + return self.is_endomorphism_set() and \ + other.base_module() is self.domain() + return False + + #### End of methods required for any Parent + + + #### Monoid methods (case of an endomorphism set) #### + + def one(self): + r""" + Return the identity element of ``self`` considered as a monoid (case of + an endomorphism set). + + This applies only when the codomain of ``self`` is equal to its domain, + i.e. when ``self`` is of the type `\mathrm{Hom}(M,M)`. + + OUTPUT: + + - the identity element of `\mathrm{End}(M) = \mathrm{Hom}(M,M)`, as an + instance of + :class:`~sage.tensor.modules.free_module_morphism.FiniteRankFreeModuleMorphism` + + EXAMPLE: + + Identity element of the set of endomorphisms of a free module + over `\ZZ`:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: H = End(M) + sage: H.one() + Identity endomorphism of Rank-3 free module M over the Integer Ring + sage: H.one().matrix(e) + [1 0 0] + [0 1 0] + [0 0 1] + sage: H.one().is_identity() + True + + NB: mathematically, ``H.one()`` coincides with the identity map of the + free module `M`. However the latter is considered here as an + element of `\mathrm{GL}(M)`, the general linear group of `M`. + Accordingly, one has to use the coercion map + `\mathrm{GL}(M) \rightarrow \mathrm{End}(M)` + to recover ``H.one()`` from ``M.identity_map()``:: + + sage: M.identity_map() + Identity map of the Rank-3 free module M over the Integer Ring + sage: M.identity_map().parent() + General linear group of the Rank-3 free module M over the Integer Ring + sage: H.one().parent() + Set of Morphisms from Rank-3 free module M over the Integer Ring to Rank-3 free module M over the Integer Ring in Category of modules over Integer Ring + sage: H.one() == H(M.identity_map()) + True + + Conversely, one can recover ``M.identity_map()`` from ``H.one()`` by + means of a conversion `\mathrm{End}(M)\rightarrow \mathrm{GL}(M)`:: + + sage: GL = M.general_linear_group() + sage: M.identity_map() == GL(H.one()) + True + + """ + if self._one is None: + if self.codomain() != self.domain(): + raise TypeError("the {} is not a monoid".format(self)) + self._one = self.element_class(self, [], is_identity=True) + return self._one + + #### End of monoid methods #### diff --git a/src/sage/tensor/modules/free_module_linear_group.py b/src/sage/tensor/modules/free_module_linear_group.py new file mode 100644 index 00000000000..b4ebc96348d --- /dev/null +++ b/src/sage/tensor/modules/free_module_linear_group.py @@ -0,0 +1,566 @@ +r""" +General linear group of a free module + +The set `\mathrm{GL}(M)` of automorphisms (i.e. invertible endomorphims) of a +free module of finite rank `M` is a group under composition of automorphisms, +named the *general linear group* of `M`. In other words, `\mathrm{GL}(M)` is +the group of units (i.e. invertible elements) of `\mathrm{End}(M)`, the +endomorphism ring of `M`. + +The group `\mathrm{GL}(M)` is implemented via the class +:class:`FreeModuleLinearGroup`. + +AUTHORS: + +- Eric Gourgoulhon (2015): initial version + +REFERENCES: + +- Chap. 15 of R. Godement: *Algebra*, Hermann (Paris) / Houghton Mifflin + (Boston) (1968) + +""" +#****************************************************************************** +# Copyright (C) 2015 Eric Gourgoulhon +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# http://www.gnu.org/licenses/ +#****************************************************************************** + +from sage.structure.unique_representation import UniqueRepresentation +from sage.structure.parent import Parent +from sage.categories.groups import Groups +from sage.tensor.modules.finite_rank_free_module import FiniteRankFreeModule +from sage.tensor.modules.free_module_automorphism import FreeModuleAutomorphism + +class FreeModuleLinearGroup(UniqueRepresentation, Parent): + r""" + General linear group of a free module of finite rank over a commutative + ring. + + Given a free module of finite rank `M` over a commutative ring `R`, the + *general linear group* of `M` is the group `\mathrm{GL}(M)` of + automorphisms (i.e. invertible endomorphims) of `M`. It is the group of + units (i.e. invertible elements) of `\mathrm{End}(M)`, the endomorphism + ring of `M`. + + This is a Sage *parent* class, whose *element* class is + :class:`~sage.tensor.modules.free_module_automorphism.FreeModuleAutomorphism`. + + INPUT: + + - ``fmodule`` -- free module `M` of finite rank over a commutative ring + `R`, as an instance of + :class:`~sage.tensor.modules.finite_rank_free_module.FiniteRankFreeModule` + + EXAMPLES: + + General linear group of a free `\ZZ`-module of rank 3:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: from sage.tensor.modules.free_module_linear_group import FreeModuleLinearGroup + sage: GL = FreeModuleLinearGroup(M) ; GL + General linear group of the Rank-3 free module M over the Integer Ring + + Instead of importing FreeModuleLinearGroup in the global name space, it is + recommended to use the module's method + :meth:`~sage.tensor.modules.finite_rank_free_module.FiniteRankFreeModule.general_linear_group`:: + + sage: GL = M.general_linear_group() ; GL + General linear group of the Rank-3 free module M over the Integer Ring + sage: latex(GL) + \mathrm{GL}\left( M \right) + + As most parents, the general linear group has a unique instance:: + + sage: GL is M.general_linear_group() + True + + `\mathrm{GL}(M)` is in the category of groups:: + + sage: GL.category() + Category of groups + sage: GL in Groups() + True + + ``GL`` is a *parent* object, whose elements are automorphisms of `M`, + represented by instances of the class + :class:`~sage.tensor.modules.free_module_automorphism.FreeModuleAutomorphism`:: + + sage: GL.Element + + sage: a = GL.an_element() ; a + Automorphism of the Rank-3 free module M over the Integer Ring + sage: a.matrix(e) + [ 1 0 0] + [ 0 -1 0] + [ 0 0 1] + sage: a in GL + True + sage: GL.is_parent_of(a) + True + + As an endomorphism, ``a`` maps elements of `M` to elements of `M`:: + + sage: v = M.an_element() ; v + Element of the Rank-3 free module M over the Integer Ring + sage: v.display() + e_0 + e_1 + e_2 + sage: a(v) + Element of the Rank-3 free module M over the Integer Ring + sage: a(v).display() + e_0 - e_1 + e_2 + + An automorphism can also be viewed as a tensor of type (1,1) on `M`:: + + sage: a.tensor_type() + (1, 1) + sage: a.display(e) + e_0*e^0 - e_1*e^1 + e_2*e^2 + sage: type(a) + + + As for any group, the identity element is obtained by the method + :meth:`one`:: + + sage: id = GL.one() ; id + Identity map of the Rank-3 free module M over the Integer Ring + sage: id*a == a + True + sage: a*id == a + True + sage: a*a^(-1) == id + True + sage: a^(-1)*a == id + True + + The identity element is of course the identity map of the module `M`:: + + sage: id(v) == v + True + sage: id.matrix(e) + [1 0 0] + [0 1 0] + [0 0 1] + + The module's changes of basis are stored as elements of the general linear + group:: + + sage: f = M.basis('f', from_family=(-e[1], 4*e[0]+3*e[2], 7*e[0]+5*e[2])) + sage: f + Basis (f_0,f_1,f_2) on the Rank-3 free module M over the Integer Ring + sage: M.change_of_basis(e,f) + Automorphism of the Rank-3 free module M over the Integer Ring + sage: M.change_of_basis(e,f) in GL + True + sage: M.change_of_basis(e,f).parent() + General linear group of the Rank-3 free module M over the Integer Ring + sage: M.change_of_basis(e,f).matrix(e) + [ 0 4 7] + [-1 0 0] + [ 0 3 5] + sage: M.change_of_basis(e,f) == M.change_of_basis(f,e).inverse() + True + + Since every automorphism is an endomorphism, there is a coercion + `\mathrm{GL}(M) \rightarrow \mathrm{End}(M)` (the endomorphism ring of + module `M`):: + + sage: End(M).has_coerce_map_from(GL) + True + + (see :class:`~sage.tensor.modules.free_module_homset.FreeModuleHomset` for + details), but not in the reverse direction, since only bijective + endomorphisms are automorphisms:: + + sage: GL.has_coerce_map_from(End(M)) + False + + A bijective endomorphism can be converted to an element of + `\mathrm{GL}(M)`:: + + sage: h = M.endomorphism([[1,0,0], [0,-1,2], [0,1,-3]]) ; h + Generic endomorphism of Rank-3 free module M over the Integer Ring + sage: h.parent() is End(M) + True + sage: ah = GL(h) ; ah + Automorphism of the Rank-3 free module M over the Integer Ring + sage: ah.parent() is GL + True + + As maps `M\rightarrow M`, ``ah`` and ``h`` are identical:: + + sage: v # recall + Element of the Rank-3 free module M over the Integer Ring + sage: ah(v) == h(v) + True + sage: ah.matrix(e) == h.matrix(e) + True + + Of course, non-invertible endomorphisms cannot be converted to elements of + `\mathrm{GL}(M)`:: + + sage: GL(M.endomorphism([[0,0,0], [0,-1,2], [0,1,-3]])) + Traceback (most recent call last): + ... + TypeError: the Generic endomorphism of Rank-3 free module M over the + Integer Ring is not invertible + + Similarly, there is a coercion `\mathrm{GL}(M)\rightarrow T^{(1,1)}(M)` + (module of type-(1,1) tensors):: + + sage: M.tensor_module(1,1).has_coerce_map_from(GL) + True + + (see :class:`~sage.tensor.modules.tensor_free_module.TensorFreeModule` for + details), but not in the reverse direction, since not every type-(1,1) + tensor can be considered as an automorphism:: + + sage: GL.has_coerce_map_from(M.tensor_module(1,1)) + False + + Invertible type-(1,1) tensors can be converted to automorphisms:: + + sage: t = M.tensor((1,1), name='t') + sage: t[e,:] = [[-1,0,0], [0,1,2], [0,1,3]] + sage: at = GL(t) ; at + Automorphism t of the Rank-3 free module M over the Integer Ring + sage: at.matrix(e) + [-1 0 0] + [ 0 1 2] + [ 0 1 3] + sage: at.matrix(e) == t[e,:] + True + + Non-invertible ones cannot:: + + sage: t0 = M.tensor((1,1), name='t_0') + sage: t0[e,0,0] = 1 + sage: t0[e,:] # the matrix is clearly not invertible + [1 0 0] + [0 0 0] + [0 0 0] + sage: GL(t0) + Traceback (most recent call last): + ... + TypeError: the Type-(1,1) tensor t_0 on the Rank-3 free module M over + the Integer Ring is not invertible + sage: t0[e,1,1], t0[e,2,2] = 2, 3 + sage: t0[e,:] # the matrix is not invertible in Mat_3(ZZ) + [1 0 0] + [0 2 0] + [0 0 3] + sage: GL(t0) + Traceback (most recent call last): + ... + TypeError: the Type-(1,1) tensor t_0 on the Rank-3 free module M over + the Integer Ring is not invertible + + """ + + Element = FreeModuleAutomorphism + + def __init__(self, fmodule): + r""" + See :class:`FreeModuleLinearGroup` for documentation and examples. + + TESTS:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: from sage.tensor.modules.free_module_linear_group import FreeModuleLinearGroup + sage: GL = FreeModuleLinearGroup(M) ; GL + General linear group of the Rank-3 free module M over the Integer Ring + sage: GL.category() + Category of groups + sage: TestSuite(GL).run() + + """ + if not isinstance(fmodule, FiniteRankFreeModule): + raise TypeError("{} is not a free module of finite rank".format( + fmodule)) + Parent.__init__(self, category=Groups()) + self._fmodule = fmodule + self._one = None # to be set by self.one() + + #### Parent methods #### + + def _element_constructor_(self, comp=[], basis=None, name=None, + latex_name=None): + r""" + Construct a free module automorphism. + + INPUT: + + - ``comp`` -- (default: ``[]``) components representing the + automorphism with respect to ``basis``; this entry can actually be + any array of size rank(M)*rank(M) from which a matrix of elements + of ``self`` base ring can be constructed; the *columns* of ``comp`` + must be the components w.r.t. ``basis`` of the images of the elements + of ``basis``. If ``comp`` is ``[]``, the automorphism has to be + initialized afterwards by method + :meth:`~sage.tensor.modules.free_module_tensor.FreeModuleTensor.set_comp` + or via the operator []. + - ``basis`` -- (default: ``None``) basis of ``self`` defining the + matrix representation; if ``None`` the default basis of ``self`` is + assumed. + - ``name`` -- (default: ``None``) name given to the automorphism + - ``latex_name`` -- (default: ``None``) LaTeX symbol to denote the + automorphism; if none is provided, the LaTeX symbol is set to ``name`` + + OUTPUT: + + - instance of + :class:`~sage.tensor.modules.free_module_automorphism.FreeModuleAutomorphism` + + EXAMPLES: + + Generic construction:: + + sage: M = FiniteRankFreeModule(ZZ, 2, name='M') + sage: e = M.basis('e') + sage: GL = M.general_linear_group() + sage: a = GL._element_constructor_(comp=[[1,2],[1,3]], basis=e, + ....: name='a') + sage: a + Automorphism a of the Rank-2 free module M over the Integer Ring + sage: a.matrix(e) + [1 2] + [1 3] + + Identity map constructed from integer 1:: + + sage: GL._element_constructor_(1) + Identity map of the Rank-2 free module M over the Integer Ring + sage: GL._element_constructor_(1).matrix(e) + [1 0] + [0 1] + + Construction from an invertible endomorphism:: + + sage: phi = M.endomorphism([[1,1], [2,3]]) + sage: a = GL._element_constructor_(phi) ; a + Automorphism of the Rank-2 free module M over the Integer Ring + sage: a.matrix(e) + [1 1] + [2 3] + sage: a.matrix(e) == phi.matrix(e) + True + + Construction from an invertible tensor of type (1,1):: + + sage: t = M.tensor((1,1), name='t') + sage: t[e,:] = [[1,1], [2,3]] + sage: a = GL._element_constructor_(t) ; a + Automorphism t of the Rank-2 free module M over the Integer Ring + sage: a.matrix(e) == t[e,:] + True + + """ + from sage.tensor.modules.free_module_tensor import FreeModuleTensor + from sage.tensor.modules.free_module_morphism import \ + FiniteRankFreeModuleMorphism + if comp == 1: + return self.one() + if isinstance(comp, FreeModuleTensor): + tens = comp # for readability + # Conversion of a type-(1,1) tensor to an automorphism + if tens.tensor_type() == (1,1): + resu = self.element_class(self._fmodule, name=tens._name, + latex_name=tens._latex_name) + for basis, comp in tens._components.iteritems(): + resu._components[basis] = comp.copy() + # Check whether the tensor is invertible: + try: + resu.inverse() + except (ZeroDivisionError, TypeError): + raise TypeError("the {} is not invertible ".format(tens)) + return resu + else: + raise TypeError("the {} cannot be converted ".format(tens) + + "to an automorphism.") + if isinstance(comp, FiniteRankFreeModuleMorphism): + # Conversion of an endomorphism to an automorphism + endo = comp # for readability + if endo.is_endomorphism() and self._fmodule is endo.domain(): + resu = self.element_class(self._fmodule, name=endo._name, + latex_name=endo._latex_name) + for basis, mat in endo._matrices.iteritems(): + resu.add_comp(basis[0])[:] = mat + # Check whether the endomorphism is invertible: + try: + resu.inverse() + except (ZeroDivisionError, TypeError): + raise TypeError("the {} is not invertible ".format(endo)) + return resu + else: + raise TypeError("cannot coerce the {}".format(endo) + + " to an element of {}".format(self)) + + # standard construction + resu = self.element_class(self._fmodule, name=name, + latex_name=latex_name) + if comp: + resu.set_comp(basis)[:] = comp + return resu + + + def _an_element_(self): + r""" + Construct some specific free module automorphism. + + OUTPUT: + + - instance of + :class:`~sage.tensor.modules.free_module_automorphism.FreeModuleAutomorphism` + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 2, name='M') + sage: e = M.basis('e') + sage: GL = M.general_linear_group() + sage: a = GL._an_element_() ; a + Automorphism of the Rank-2 free module M over the Integer Ring + sage: a.matrix(e) + [ 1 0] + [ 0 -1] + + """ + resu = self.element_class(self._fmodule) + if self._fmodule._def_basis is not None: + comp = resu.set_comp() + for i in self._fmodule.irange(): + if i%2 == 0: + comp[[i,i]] = self._fmodule._ring.one() + else: + comp[[i,i]] = -(self._fmodule._ring.one()) + return resu + + #### End of parent methods #### + + #### Monoid methods #### + + def one(self): + r""" + Return the group identity element of ``self``. + + The group identity element is nothing but the module identity map. + + OUTPUT: + + - instance of + :class:`~sage.tensor.modules.free_module_automorphism.FreeModuleAutomorphism` + representing the identity element. + + EXAMPLES: + + Identity element of the general linear group of a rank-2 free module:: + + sage: M = FiniteRankFreeModule(ZZ, 2, name='M', start_index=1) + sage: GL = M.general_linear_group() + sage: GL.one() + Identity map of the Rank-2 free module M over the Integer Ring + + The identity element is cached:: + + sage: GL.one() is GL.one() + True + + Check that the element returned is indeed the neutral element for + the group law:: + + sage: e = M.basis('e') + sage: a = GL([[3,4],[5,7]], basis=e) ; a + Automorphism of the Rank-2 free module M over the Integer Ring + sage: a.matrix(e) + [3 4] + [5 7] + sage: GL.one() * a == a + True + sage: a * GL.one() == a + True + sage: a * a^(-1) == GL.one() + True + sage: a^(-1) * a == GL.one() + True + + The unit element of `\mathrm{GL}(M)` is the identity map of `M`:: + + sage: GL.one()(e[1]) + Element e_1 of the Rank-2 free module M over the Integer Ring + sage: GL.one()(e[2]) + Element e_2 of the Rank-2 free module M over the Integer Ring + + Its matrix is the identity matrix in any basis:: + + sage: GL.one().matrix(e) + [1 0] + [0 1] + sage: f = M.basis('f', from_family=(e[1]+2*e[2], e[1]+3*e[2])) + sage: GL.one().matrix(f) + [1 0] + [0 1] + + """ + if self._one is None: + self._one = self.element_class(self._fmodule, is_identity=True) + # Initialization of the components (Kronecker delta) in some basis: + if self._fmodule.bases(): + self._one.components(self._fmodule.bases()[0]) + return self._one + + #### End of monoid methods #### + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLE:: + + sage: M = FiniteRankFreeModule(ZZ, 2, name='M') + sage: GL = M.general_linear_group() + sage: GL._repr_() + 'General linear group of the Rank-2 free module M over the Integer Ring' + + """ + return "General linear group of the {}".format(self._fmodule) + + def _latex_(self): + r""" + Return a string representation of ``self``. + + EXAMPLE:: + + sage: M = FiniteRankFreeModule(ZZ, 2, name='M') + sage: GL = M.general_linear_group() + sage: GL._latex_() + \mathrm{GL}\left( M \right) + + """ + from sage.misc.latex import latex + return r"\mathrm{GL}\left("+ latex(self._fmodule)+ r"\right)" + + + def base_module(self): + r""" + Return the free module of which ``self`` is the general linear group. + + OUTPUT: + + - instance of :class:`FiniteRankFreeModule` representing the free + module of which ``self`` is the general linear group + + EXAMPLE:: + + sage: M = FiniteRankFreeModule(ZZ, 2, name='M') + sage: GL = M.general_linear_group() + sage: GL.base_module() + Rank-2 free module M over the Integer Ring + sage: GL.base_module() is M + True + + """ + return self._fmodule diff --git a/src/sage/tensor/modules/free_module_morphism.py b/src/sage/tensor/modules/free_module_morphism.py new file mode 100644 index 00000000000..e4fa5efa6a2 --- /dev/null +++ b/src/sage/tensor/modules/free_module_morphism.py @@ -0,0 +1,1281 @@ +r""" +Free module morphisms + +The class :class:`FiniteRankFreeModuleMorphism` implements homomorphisms +between two free modules of finite rank over the same commutative ring. + +AUTHORS: + +- Eric Gourgoulhon, Michal Bejger (2014-2015): initial version + +REFERENCES: + +- Chaps. 13, 14 of R. Godement: *Algebra*, Hermann (Paris) / Houghton Mifflin + (Boston) (1968) +- Chap. 3 of S. Lang: *Algebra*, 3rd ed., Springer (New York) (2002) + +""" +#****************************************************************************** +# Copyright (C) 2015 Eric Gourgoulhon +# Copyright (C) 2015 Michal Bejger +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# http://www.gnu.org/licenses/ +#****************************************************************************** + +from sage.rings.integer import Integer +from sage.categories.morphism import Morphism +from sage.categories.homset import Hom +from sage.tensor.modules.finite_rank_free_module import FiniteRankFreeModule + +class FiniteRankFreeModuleMorphism(Morphism): + r""" + Homomorphism between free modules of finite rank over a commutative ring. + + An instance of this class is a homomorphism + + .. MATH:: + + \phi:\ M \longrightarrow N, + + where `M` and `N` are two free modules of finite rank over the same + commutative ring `R`. + + This is a Sage *element* class, the corresponding *parent* class being + :class:`~sage.tensor.modules.free_module_homset.FreeModuleHomset`. + + INPUT: + + - ``parent`` -- hom-set Hom(M,N) to which the homomorphism belongs + - ``matrix_rep`` -- matrix representation of the homomorphism with + respect to the bases ``bases``; this entry can actually + be any material from which a matrix of size rank(N)*rank(M) of + elements of `R` can be constructed; the *columns* of the matrix give + the images of the basis of `M` (see the convention in the example below) + - ``bases`` -- (default: ``None``) pair (basis_M, basis_N) defining the + matrix representation, basis_M being a basis of module `M` and + basis_N a basis of module `N` ; if None the pair formed by the + default bases of each module is assumed. + - ``name`` -- (default: ``None``) string; name given to the homomorphism + - ``latex_name`` -- (default: ``None``) string; LaTeX symbol to denote the + homomorphism; if None, ``name`` will be used. + - ``is_identity`` -- (default: ``False``) determines whether the + constructed object is the identity endomorphism; if set to ``True``, then + N must be M and the entry ``matrix_rep`` is not used. + + EXAMPLES: + + A homomorphism between two free modules over `\ZZ` is contructed + as an element of the corresponding hom-set, by means of the function + ``__call__``:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: N = FiniteRankFreeModule(ZZ, 2, name='N') + sage: e = M.basis('e') ; f = N.basis('f') + sage: H = Hom(M,N) ; H + Set of Morphisms from Rank-3 free module M over the Integer Ring to + Rank-2 free module N over the Integer Ring in Category of modules + over Integer Ring + sage: phi = H([[2,-1,3], [1,0,-4]], name='phi', latex_name=r'\phi') ; phi + Generic morphism: + From: Rank-3 free module M over the Integer Ring + To: Rank-2 free module N over the Integer Ring + + Since no bases have been specified in the argument list, the provided + matrix is relative to the default bases of modules M and N, so that + the above is equivalent to:: + + sage: phi = H([[2,-1,3], [1,0,-4]], bases=(e,f), name='phi', + ....: latex_name=r'\phi') ; phi + Generic morphism: + From: Rank-3 free module M over the Integer Ring + To: Rank-2 free module N over the Integer Ring + + An alternative way to construct a homomorphism is to call the method + :meth:`~sage.tensor.modules.finite_rank_free_module.FiniteRankFreeModule.hom` + on the domain:: + + sage: phi = M.hom(N, [[2,-1,3], [1,0,-4]], bases=(e,f), name='phi', + ....: latex_name=r'\phi') ; phi + Generic morphism: + From: Rank-3 free module M over the Integer Ring + To: Rank-2 free module N over the Integer Ring + + The parent of a homomorphism is of course the corresponding hom-set:: + + sage: phi.parent() is H + True + sage: phi.parent() is Hom(M,N) + True + + Due to Sage's category scheme, the actual class of the homomorphism ``phi`` + is a derived class of :class:`FiniteRankFreeModuleMorphism`:: + + sage: type(phi) + + sage: isinstance(phi, sage.tensor.modules.free_module_morphism.FiniteRankFreeModuleMorphism) + True + + The domain and codomain of the homomorphism are returned respectively by + the methods ``domain()`` and ``codomain()``, which are implemented as + Sage's constant functions:: + + sage: phi.domain() + Rank-3 free module M over the Integer Ring + sage: phi.codomain() + Rank-2 free module N over the Integer Ring + sage: type(phi.domain) + + + The matrix of the homomorphism with respect to a pair of bases is + returned by the method :meth:`matrix`:: + + sage: phi.matrix(e,f) + [ 2 -1 3] + [ 1 0 -4] + + The convention is that the columns of this matrix give the components of + the images of the elements of basis ``e`` w.r.t basis ``f``:: + + sage: phi(e[0]).display() + phi(e_0) = 2 f_0 + f_1 + sage: phi(e[1]).display() + phi(e_1) = -f_0 + sage: phi(e[2]).display() + phi(e_2) = 3 f_0 - 4 f_1 + + Test of the module homomorphism laws:: + + sage: phi(M.zero()) == N.zero() + True + sage: u = M([1,2,3], basis=e, name='u') ; u.display() + u = e_0 + 2 e_1 + 3 e_2 + sage: v = M([-2,1,4], basis=e, name='v') ; v.display() + v = -2 e_0 + e_1 + 4 e_2 + sage: phi(u).display() + phi(u) = 9 f_0 - 11 f_1 + sage: phi(v).display() + phi(v) = 7 f_0 - 18 f_1 + sage: phi(3*u + v).display() + 34 f_0 - 51 f_1 + sage: phi(3*u + v) == 3*phi(u) + phi(v) + True + + The identity endomorphism:: + + sage: Id = End(M).one() ; Id + Identity endomorphism of Rank-3 free module M over the Integer Ring + sage: Id.parent() + Set of Morphisms from Rank-3 free module M over the Integer Ring to + Rank-3 free module M over the Integer Ring in Category of modules + over Integer Ring + sage: Id.parent() is End(M) + True + + The matrix of Id with respect to the basis e is of course the identity + matrix:: + + sage: Id.matrix(e) + [1 0 0] + [0 1 0] + [0 0 1] + + The identity acting on a module element:: + + sage: Id(v) is v + True + + """ + def __init__(self, parent, matrix_rep, bases=None, name=None, + latex_name=None, is_identity=False): + r""" + TESTS: + + Generic homomorphism:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: N = FiniteRankFreeModule(ZZ, 2, name='N') + sage: e = M.basis('e') ; f = N.basis('f') + sage: from sage.tensor.modules.free_module_morphism import FiniteRankFreeModuleMorphism + sage: phi = FiniteRankFreeModuleMorphism(Hom(M,N), [[1,0,-3], [2,1,4]], + ....: name='phi', latex_name=r'\phi') + sage: phi + Generic morphism: + From: Rank-3 free module M over the Integer Ring + To: Rank-2 free module N over the Integer Ring + sage: phi.matrix(e,f) + [ 1 0 -3] + [ 2 1 4] + sage: latex(phi) + \phi + + Generic endomorphism:: + + sage: phi = FiniteRankFreeModuleMorphism(End(M), [[1,0,-3], [2,1,4], [7,8,9]], + ....: name='phi', latex_name=r'\phi') + sage: phi + Generic endomorphism of Rank-3 free module M over the Integer Ring + + Identity endomorphism:: + + sage: phi = FiniteRankFreeModuleMorphism(End(M), 'whatever', is_identity=True) + sage: phi + Identity endomorphism of Rank-3 free module M over the Integer Ring + sage: phi.matrix(e) + [1 0 0] + [0 1 0] + [0 0 1] + sage: latex(phi) + \mathrm{Id} + + """ + from sage.matrix.constructor import matrix + from sage.misc.constant_function import ConstantFunction + Morphism.__init__(self, parent) + fmodule1 = parent.domain() + fmodule2 = parent.codomain() + if bases is None: + def_basis1 = fmodule1.default_basis() + if def_basis1 is None: + raise ValueError("the {} has no default ".format(fmodule1) + + "basis") + def_basis2 = fmodule2.default_basis() + if def_basis2 is None: + raise ValueError("the {} has no default ".format(fmodule2) + + "basis") + bases = (def_basis1, def_basis2) + else: + bases = tuple(bases) # insures bases is a tuple + if len(bases) != 2: + raise TypeError("the argument bases must contain 2 bases") + if bases[0] not in fmodule1.bases(): + raise TypeError("{} is not a basis on the {}".format(bases[0], + fmodule1)) + if bases[1] not in fmodule2.bases(): + raise TypeError("{} is not a basis on the {}".format(bases[1], + fmodule2)) + ring = parent.base_ring() + n1 = fmodule1.rank() + n2 = fmodule2.rank() + if is_identity: + # Construction of the identity endomorphism + if fmodule1 != fmodule2: + raise TypeError("the domain and codomain must coincide " + \ + "for the identity endomorphism.") + if bases[0] != bases[1]: + raise TypeError("the two bases must coincide for " + \ + "constructing the identity endomorphism.") + self._is_identity = True + zero = ring.zero() + one = ring.one() + matrix_rep = [] + for i in range(n1): + row = [zero]*n1 + row[i] = one + matrix_rep.append(row) + if name is None: + name = 'Id' + if latex_name is None and name == 'Id': + latex_name = r'\mathrm{Id}' + self._repr_type_str = 'Identity' + else: + # Construction of a generic morphism + self._is_identity = False + if isinstance(matrix_rep, ConstantFunction): + # the zero morphism + if matrix_rep().is_zero(): + matrix_rep = 0 + if matrix_rep == 1: + if fmodule1 == fmodule2: + # the identity endomorphism (again): + self._is_identity = True + self._repr_type_str = 'Identity' + name = 'Id' + latex_name = r'\mathrm{Id}' + self._matrices = {bases: matrix(ring, n2, n1, matrix_rep)} + self._name = name + if latex_name is None: + self._latex_name = self._name + else: + self._latex_name = latex_name + + # + # SageObject methods + # + + def _latex_(self): + r""" + LaTeX representation of the object. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: N = FiniteRankFreeModule(ZZ, 2, name='N') + sage: e = M.basis('e') ; f = N.basis('f') + sage: phi = M.hom(N, [[-1,2,0], [5,1,2]], name='phi', + ....: latex_name=r'\Phi') + sage: phi._latex_() + '\\Phi' + sage: latex(phi) # indirect doctest + \Phi + + :: + + sage: phi = M.hom(N, [[-1,2,0], [5,1,2]], name='F') + sage: phi._latex_() + 'F' + + :: + + sage: phi = M.hom(N, [[-1,2,0], [5,1,2]]) + sage: phi._latex_() + '\\mbox{Generic morphism:\n From: Rank-3 free module M over the Integer Ring\n To: Rank-2 free module N over the Integer Ring}' + + """ + if self._latex_name is None: + return r'\mbox{' + str(self) + r'}' + else: + return self._latex_name + + def __eq__(self, other): + r""" + Comparison (equality) operator. + + INPUT: + + - ``other`` -- a free module morphism (or 0) + + OUTPUT: + + - ``True`` if ``self`` is equal to ``other`` and ``False`` otherwise + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: N = FiniteRankFreeModule(ZZ, 2, name='N') + sage: e = M.basis('e') ; f = N.basis('f') + sage: phi = M.hom(N, [[-1,2,0], [5,1,2]], name='phi', + ....: latex_name=r'\phi') + sage: psi = M.hom(N, [[-1,2,0], [5,1,2]]) + sage: phi.__eq__(psi) + True + sage: phi == psi + True + sage: phi.__eq__(phi) + True + sage: phi.__eq__(+phi) + True + sage: psi = M.hom(N, [[1,1,0], [4,1,3]]) + sage: phi.__eq__(psi) + False + sage: phi.__eq__(-phi) + False + + Comparison of homomorphisms defined on different bases:: + + sage: a = M.automorphism() ; a[0,2], a[1,0], a[2,1] = 1, -1, -1 + sage: ep = e.new_basis(a, 'ep', latex_symbol="e'") + sage: psi = M.hom(N, [[-2,0,-1], [-1,-2, 5]], bases=(ep,f)) + sage: phi.__eq__(psi) + True + sage: phi.matrix(e,f) == psi.matrix(e,f) # check + True + + Comparison of homomorphisms having the same matrix but defined on + different modules:: + + sage: N1 = FiniteRankFreeModule(ZZ, 2, name='N1') + sage: f1 = N1.basis('f') + sage: phi1 = M.hom(N1, [[-1,2,0], [5,1,2]]) + sage: phi.matrix() == phi1.matrix() # same matrix in the default bases + True + sage: phi.__eq__(phi1) + False + + Comparison to zero:: + + sage: phi.__eq__(0) + False + sage: phi = M.hom(N, 0) + sage: phi.__eq__(0) + True + sage: phi == 0 + True + sage: phi.__eq__(Hom(M,N).zero()) + True + + """ + if isinstance(other, (int, Integer)): # other should be 0 + if other == 0: + return self.is_zero() + else: + return False + elif not isinstance(other, FiniteRankFreeModuleMorphism): + return False + elif self.parent() != other.parent(): + return False + else: + bases = self._common_bases(other) + if bases is None: + raise ValueError("no common pair of bases has been found to " + + "compare {} and {}".format(self, other)) + return bool( self.matrix(*bases) == other.matrix(*bases) ) + + def __ne__(self, other): + r""" + Inequality operator. + + INPUT: + + - ``other`` -- a free module morphism (or 0) + + OUTPUT: + + - ``True`` if ``self`` is different from ``other`` and ``False`` + otherwise + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: N = FiniteRankFreeModule(ZZ, 2, name='N') + sage: e = M.basis('e') ; f = N.basis('f') + sage: phi = M.hom(N, [[-1,2,0], [5,1,2]], name='phi', + ....: latex_name=r'\phi') + sage: psi = M.hom(N, [[-1,2,0], [5,1,2]]) + sage: phi.__ne__(psi) + False + sage: psi = M.hom(N, [[1,1,0], [4,1,3]]) + sage: phi.__ne__(psi) + True + sage: phi != psi + True + sage: phi.__ne__('junk') + True + sage: Hom(M,N).zero().__ne__(0) + False + + """ + return not self.__eq__(other) + + def __cmp__(self, other): + r""" + Old-style (Python 2) comparison operator. + + This is provisory, until migration to Python 3 is achieved. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: N = FiniteRankFreeModule(ZZ, 2, name='N') + sage: e = M.basis('e') ; f = N.basis('f') + sage: phi = M.hom(N, [[-1,2,0], [5,1,2]], name='phi', + ....: latex_name=r'\phi') + sage: psi = M.hom(N, [[-1,2,0], [5,1,2]]) + sage: phi.__cmp__(psi) + 0 + sage: phi.__cmp__(phi) + 0 + sage: phi.__cmp__(phi+phi) + -1 + sage: phi.__cmp__(2*psi) + -1 + sage: phi.__cmp__(-phi) + -1 + + """ + if self.__eq__(other): + return 0 + else: + return -1 + + # + # Required module methods + # + + def __nonzero__(self): + r""" + Return ``True`` if ``self`` is nonzero and ``False`` otherwise. + + This method is called by self.is_zero(). + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: N = FiniteRankFreeModule(ZZ, 2, name='N') + sage: e = M.basis('e') ; f = N.basis('f') + sage: phi = M.hom(N, [[2,-1,3], [1,0,-4]]) + sage: phi.__nonzero__() + True + sage: phi.is_zero() # indirect doctest + False + sage: phi = M.hom(N, 0) + sage: phi.__nonzero__() + False + sage: phi.is_zero() # indirect doctest + True + sage: Hom(M,N).zero().__nonzero__() + False + + """ + # Some matrix representation is picked at random: + matrix_rep = self._matrices.values()[0] + return not matrix_rep.is_zero() + + def _add_(self, other): + r""" + Homomorphism addition. + + INPUT: + + - ``other`` -- a free module morphism (same parent as ``self``) + + OUPUT: + + - the homomorphism resulting from the addition of ``self`` and ``other`` + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: N = FiniteRankFreeModule(ZZ, 2, name='N') + sage: e = M.basis('e') ; f = N.basis('f') + sage: phi = M.hom(N, [[-1,2,0], [5,1,2]], name='phi', + ....: latex_name=r'\phi') + sage: psi = M.hom(N, [[1,1,0], [4,1,3]]) + sage: s = phi._add_(psi) ; s + Generic morphism: + From: Rank-3 free module M over the Integer Ring + To: Rank-2 free module N over the Integer Ring + sage: s.matrix(e,f) + [0 3 0] + [9 2 5] + sage: s.matrix(e,f) == phi.matrix(e,f) + psi.matrix(e,f) # check + True + sage: s == phi + psi # indirect doctest + True + + Addition of homomorphisms defined on different bases:: + + sage: a = M.automorphism() ; a[0,2], a[1,0], a[2,1] = 1, -1, -1 + sage: ep = e.new_basis(a, 'ep', latex_symbol="e'") + sage: b = N.automorphism() ; b[0,1], b[1,0] = -1, 1 + sage: fp = f.new_basis(b, 'fp', latex_symbol="f'") + sage: psi = M.hom(N, [[-2,0,-1], [-1,-2, 5]], bases=(ep,fp)) + sage: s = phi._add_(psi) ; s + Generic morphism: + From: Rank-3 free module M over the Integer Ring + To: Rank-2 free module N over the Integer Ring + sage: s.matrix(e,f) + [-6 1 -2] + [ 4 3 2] + sage: s.matrix(e,f) == phi.matrix(e,f) + psi.matrix(e,f) # check + True + sage: s == phi + psi # indirect doctest + True + + Other tests:: + + sage: phi._add_(Hom(M,N).zero()) == phi + True + + """ + # No need for consistency checks since self and other are guaranteed + # to have the same parents + bases = self._common_bases(other) + if bases is None: + raise ValueError("no common pair of bases has been found to " + + "add {} and {}".format(self, other)) + # Addition at the matrix level: + resu_mat = self._matrices[bases] + other._matrices[bases] + if self._name is not None and other._name is not None: + resu_name = self._name + '+' + other._name + else: + resu_name = None + if self._latex_name is not None and other._latex_name is not None: + resu_latex_name = self._latex_name + '+' + other._latex_name + else: + resu_latex_name = None + return self.__class__(self.parent(), resu_mat, bases=bases, + name=resu_name, latex_name=resu_latex_name) + + def _sub_(self, other): + r""" + Homomorphism subtraction. + + INPUT: + + - ``other`` -- a free module morphism (same parent as ``self``) + + OUPUT: + + - the homomorphism resulting from the subtraction of ``other`` from + ``self`` + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: N = FiniteRankFreeModule(ZZ, 2, name='N') + sage: e = M.basis('e') ; f = N.basis('f') + sage: phi = M.hom(N, [[-1,2,0], [5,1,2]], name='phi', + ....: latex_name=r'\phi') + sage: psi = M.hom(N, [[1,1,0], [4,1,3]]) + sage: s = phi._sub_(psi) ; s + Generic morphism: + From: Rank-3 free module M over the Integer Ring + To: Rank-2 free module N over the Integer Ring + sage: s.matrix(e,f) + [-2 1 0] + [ 1 0 -1] + sage: s.matrix(e,f) == phi.matrix(e,f) - psi.matrix(e,f) # check + True + sage: s == phi - psi # indirect doctest + True + + Subtraction of homomorphisms defined on different bases:: + + sage: a = M.automorphism() ; a[0,2], a[1,0], a[2,1] = 1, -1, -1 + sage: ep = e.new_basis(a, 'ep', latex_symbol="e'") + sage: b = N.automorphism() ; b[0,1], b[1,0] = -1, 1 + sage: fp = f.new_basis(b, 'fp', latex_symbol="f'") + sage: psi = M.hom(N, [[-2,0,-1], [-1,-2, 5]], bases=(ep,fp)) + sage: s = phi._sub_(psi) ; s + Generic morphism: + From: Rank-3 free module M over the Integer Ring + To: Rank-2 free module N over the Integer Ring + sage: s.matrix(e,f) + [ 4 3 2] + [ 6 -1 2] + sage: s.matrix(e,f) == phi.matrix(e,f) - psi.matrix(e,f) # check + True + sage: s == phi - psi # indirect doctest + True + + Other tests:: + + sage: phi._sub_(Hom(M,N).zero()) == phi + True + sage: Hom(M,N).zero()._sub_(phi) == -phi + True + sage: phi._sub_(phi).is_zero() + True + + """ + # No need for consistency checks since self and other are guaranteed + # to have the same parents + bases = self._common_bases(other) + if bases is None: + raise ValueError("no common pair of bases has been found to " + + "subtract {} from {}".format(other, self)) + # Subtraction at the matrix level: + resu_mat = self._matrices[bases] - other._matrices[bases] + if self._name is not None and other._name is not None: + resu_name = self._name + '-' + other._name + else: + resu_name = None + if self._latex_name is not None and other._latex_name is not None: + resu_latex_name = self._latex_name + '-' + other._latex_name + else: + resu_latex_name = None + return self.__class__(self.parent(), resu_mat, bases=bases, + name=resu_name, latex_name=resu_latex_name) + + def _rmul_(self, scalar): + r""" + Multiplication on the left by ``scalar``. + + INPUT: + + - ``scalar`` -- element of the ring over which the parent of ``self`` + is a module. + + OUPUT: + + - the homomorphism resulting from the multiphication of ``self`` by + ``scalar`` + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: N = FiniteRankFreeModule(ZZ, 2, name='N') + sage: e = M.basis('e') ; f = N.basis('f') + sage: phi = M.hom(N, [[-1,2,0], [5,1,2]], name='phi', + ....: latex_name=r'\phi') + sage: s = phi._rmul_(7) ; s + Generic morphism: + From: Rank-3 free module M over the Integer Ring + To: Rank-2 free module N over the Integer Ring + sage: s.matrix(e,f) + [-7 14 0] + [35 7 14] + sage: s == 7*phi # indirect doctest + True + + """ + resu = self.__class__(self.parent(), 0) # 0 = provisory value + for bases, mat in self._matrices.iteritems(): + resu._matrices[bases] = scalar * mat + return resu + + + # + # Other module methods + # + + def __pos__(self): + r""" + Unary plus operator. + + OUTPUT: + + - an exact copy of ``self`` + + EXAMPLE:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: N = FiniteRankFreeModule(ZZ, 2, name='N') + sage: e = M.basis('e') ; f = N.basis('f') + sage: phi = M.hom(N, [[-1,2,0], [5,1,2]], name='phi', + ....: latex_name=r'\phi') + sage: s = phi.__pos__() ; s + Generic morphism: + From: Rank-3 free module M over the Integer Ring + To: Rank-2 free module N over the Integer Ring + sage: s == +phi + True + sage: s == phi + True + sage: s is phi + False + + """ + resu = self.__class__(self.parent(), 0, is_identity=self._is_identity) + # 0 = provisory value + for bases, mat in self._matrices.iteritems(): + resu._matrices[bases] = +mat + if self._name is not None: + resu._name = '+' + self._name + if self._latex_name is not None: + resu._latex_name = '+' + self._latex_name + return resu + + def __neg__(self): + r""" + Unary minus operator. + + OUTPUT: + + - the homomorphism `-f`, where `f` is ``self`` + + EXAMPLE:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: N = FiniteRankFreeModule(ZZ, 2, name='N') + sage: e = M.basis('e') ; f = N.basis('f') + sage: phi = M.hom(N, [[-1,2,0], [5,1,2]], name='phi', + ....: latex_name=r'\phi') + sage: s = phi.__neg__() ; s + Generic morphism: + From: Rank-3 free module M over the Integer Ring + To: Rank-2 free module N over the Integer Ring + sage: s == -phi + True + sage: s.matrix() + [ 1 -2 0] + [-5 -1 -2] + sage: s.matrix() == -phi.matrix() + True + + """ + resu = self.__class__(self.parent(), 0) # 0 = provisory value + for bases, mat in self._matrices.iteritems(): + resu._matrices[bases] = -mat + if self._name is not None: + resu._name = '-' + self._name + if self._latex_name is not None: + resu._latex_name = '-' + self._latex_name + return resu + + # + # Map methods + # + + def _call_(self, element): + r""" + Action of the homomorphism ``self`` on some free module element + + INPUT: + + - ``element`` -- element of the domain of ``self`` + + OUTPUT: + + - the image of ``element`` by ``self`` + + EXAMPLE: + + Images of a homomorphism between two `\ZZ`-modules:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: N = FiniteRankFreeModule(ZZ, 2, name='N') + sage: e = M.basis('e') ; f = N.basis('f') + sage: phi = M.hom(N, [[-1,2,0], [5,1,2]], name='phi', latex_name=r'\phi') + sage: v = M([1,2,3], basis=e, name='v') + sage: w = phi(v) ; w + Element phi(v) of the Rank-2 free module N over the Integer Ring + sage: w.display() + phi(v) = 3 f_0 + 13 f_1 + + Tests:: + + sage: for i in range(2): + ....: print w[i] == sum( phi.matrix()[i,j]*v[j] for j in range(3) ), + ....: + True True + sage: phi.matrix(e,f) + [-1 2 0] + [ 5 1 2] + sage: phi(e[0]).display() + phi(e_0) = -f_0 + 5 f_1 + sage: phi(e[1]).display() + phi(e_1) = 2 f_0 + f_1 + sage: phi(e[2]).display() + phi(e_2) = 2 f_1 + + Image of an element that is not defined on the default basis:: + + sage: a = M.automorphism() + sage: a[0,2], a[1,0], a[2,1] = 1, -1, -1 + sage: ep = e.new_basis(a, 'ep', latex_symbol="e'") + sage: v = M([1,2,3], basis=ep, name='v') + sage: w = phi(v) ; w + Element phi(v) of the Rank-2 free module N over the Integer Ring + sage: w.display() + phi(v) = -5 f_0 + 10 f_1 + sage: for i in range(2): + ....: print w[i] == sum( phi.matrix(ep,f)[i,j]*v[ep,j] for j in range(3) ), + ....: + True True + + Check of homomorphism properties:: + + sage: phi(M.zero()) == N.zero() + True + + """ + if self._is_identity: + return element + dom = self.parent().domain() + sindex = dom._sindex + codom = self.parent().codomain() + basis_codom = codom.default_basis() + # Search for a common basis to compute the result + for basis in element._components: + try: + self.matrix(basis, basis_codom) + basis_dom = basis + break + except ValueError: + continue + else: + raise ValueError("no common basis found to evaluate the image " + + "of {} by {}".format(element,self)) + # Components of the result obtained by matrix multiplication + mat = self.matrix(basis_dom, basis_codom) + vcomp = element._components[basis_dom] + tresu = [] + for i in range(codom.rank()): + s = 0 + for j in range(dom.rank()): + s += mat[i,j] * vcomp[[j+sindex]] + tresu.append(s) + # Name of the result + if self._name is not None and element._name is not None: + resu_name = self._name + '(' + element._name + ')' + else: + resu_name = None + if self._latex_name is not None and element._latex_name is not None: + resu_latex_name = self._latex_name + r'\left(' + \ + element._latex_name + r'\right)' + else: + resu_latex_name = None + # Creation of the result + return codom(tresu, basis=basis_codom, name=resu_name, + latex_name=resu_latex_name) + + def is_injective(self): + r""" + Determine whether ``self`` is injective. + + OUTPUT: + + - ``True`` if ``self`` is an injective homomorphism and ``False`` + otherwise + + EXAMPLES: + + Homomorphisms between two `\ZZ`-modules:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: N = FiniteRankFreeModule(ZZ, 2, name='N') + sage: e = M.basis('e') ; f = N.basis('f') + sage: phi = M.hom(N, [[-1,2,0], [5,1,2]]) + sage: phi.matrix(e,f) + [-1 2 0] + [ 5 1 2] + sage: phi.is_injective() + False + + Indeed, phi has a non trivial kernel:: + + sage: phi(4*e[0] + 2*e[1] - 11*e[2]).display() + 0 + + An injective homomorphism:: + + sage: psi = N.hom(M, [[1,-1], [0,3], [4,-5]]) + sage: psi.matrix(f,e) + [ 1 -1] + [ 0 3] + [ 4 -5] + sage: psi.is_injective() + True + + Of course, the identity endomorphism is injective:: + + sage: End(M).one().is_injective() + True + sage: End(N).one().is_injective() + True + + """ + # Some matrix representation is picked at random: + matrix_rep = self._matrices.values()[0] + return matrix_rep.right_kernel().rank() == 0 + + def is_surjective(self): + r""" + Determine whether ``self`` is surjective. + + OUTPUT: + + - ``True`` if ``self`` is a surjective homomorphism and ``False`` + otherwise + + EXAMPLE: + + This method has not been implemented yet:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: N = FiniteRankFreeModule(ZZ, 2, name='N') + sage: e = M.basis('e') ; f = N.basis('f') + sage: phi = M.hom(N, [[-1,2,0], [5,1,2]]) + sage: phi.is_surjective() + Traceback (most recent call last): + ... + NotImplementedError: FiniteRankFreeModuleMorphism.is_surjective() + has not been implemented yet + + except for the identity endomorphisme (!):: + + sage: End(M).one().is_surjective() + True + sage: End(N).one().is_surjective() + True + + """ + if self._is_identity: + return True + raise NotImplementedError( + "FiniteRankFreeModuleMorphism.is_surjective() " + + "has not been implemented yet") + # + # Morphism methods + # + + def is_identity(self): + r""" + Check whether ``self`` is the identity morphism. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 2, name='M') + sage: e = M.basis('e') + sage: phi = M.endomorphism([[1,0], [0,1]]) + sage: phi.is_identity() + True + sage: (phi+phi).is_identity() + False + sage: End(M).zero().is_identity() + False + sage: a = M.automorphism() ; a[0,1], a[1,0] = 1, -1 + sage: ep = e.new_basis(a, 'ep', latex_symbol="e'") + sage: phi = M.endomorphism([[1,0], [0,1]], basis=ep) + sage: phi.is_identity() + True + + Example illustrating that the identity can be constructed from a + matrix that is not the identity one, provided that it is relative to + different bases:: + + sage: phi = M.hom(M, [[0,1], [-1,0]], bases=(ep,e)) + sage: phi.is_identity() + True + + Of course, if we ask for the matrix in a single basis, it is the + identity matrix:: + + sage: phi.matrix(e) + [1 0] + [0 1] + sage: phi.matrix(ep) + [1 0] + [0 1] + + """ + if self._is_identity: + return True + # The identity must be an endomorphism: + fmodule = self.domain() + if fmodule != self.codomain(): + return False + # Some basis in which ``self`` has a representation is picked at + # random and the test is performed on the images of the basis + # elements: + basis = self._matrices.keys()[0][0] + for i in fmodule.irange(): + if self(basis[i]) != basis[i]: + return False + self._is_identity = True + return True + + # + # End of Morphism methods + # + + def matrix(self, basis1=None, basis2=None): + r""" + Return the matrix of ``self`` w.r.t to a pair of bases. + + If the matrix is not known already, it is computed from the matrix in + another pair of bases by means of the change-of-basis formula. + + INPUT: + + - ``basis1`` -- (default: ``None``) basis of the domain of ``self``; if + none is provided, the domain's default basis is assumed + - ``basis2`` -- (default: ``None``) basis of the codomain of ``self``; + if none is provided, ``basis2`` is set to ``basis1`` if ``self`` is + an endomorphism, otherwise, ``basis2`` is set to the codomain's + default basis. + + OUTPUT: + + - the matrix representing representing the homomorphism ``self`` w.r.t + to bases ``basis1`` and ``basis2``; more precisely, the columns of + this matrix are formed by the components w.r.t. ``basis2`` of + the images of the elements of ``basis1``. + + EXAMPLES: + + Matrix of a homomorphism between two `\ZZ`-modules:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: N = FiniteRankFreeModule(ZZ, 2, name='N') + sage: e = M.basis('e') ; f = N.basis('f') + sage: phi = M.hom(N, [[-1,2,0], [5,1,2]]) + sage: phi.matrix() # default bases + [-1 2 0] + [ 5 1 2] + sage: phi.matrix(e,f) # bases explicited + [-1 2 0] + [ 5 1 2] + sage: type(phi.matrix()) + + + Matrix in bases different from those in which the homomorphism has + been defined:: + + sage: a = M.automorphism(matrix=[[-1,0,0],[0,1,2],[0,1,3]], basis=e) + sage: ep = e.new_basis(a, 'ep', latex_symbol="e'") + sage: b = N.automorphism(matrix=[[3,5],[4,7]], basis=f) + sage: fp = f.new_basis(b, 'fp', latex_symbol="f'") + sage: phi.matrix(ep, fp) + [ 32 -1 -12] + [-19 1 8] + + Check of the change-of-basis formula:: + + sage: phi.matrix(ep, fp) == (b^(-1)).matrix(f) * phi.matrix(e,f) * a.matrix(e) + True + + Single change of basis:: + + sage: phi.matrix(ep, f) + [ 1 2 4] + [-5 3 8] + sage: phi.matrix(ep,f) == phi.matrix(e,f) * a.matrix(e) + True + sage: phi.matrix(e, fp) + [-32 9 -10] + [ 19 -5 6] + sage: phi.matrix(e, fp) == (b^(-1)).matrix(f) * phi.matrix(e,f) + True + + Matrix of an endomorphism:: + + sage: phi = M.endomorphism([[1,2,3], [4,5,6], [7,8,9]], basis=ep) + sage: phi.matrix(ep) + [1 2 3] + [4 5 6] + [7 8 9] + sage: phi.matrix(ep,ep) # same as above + [1 2 3] + [4 5 6] + [7 8 9] + sage: phi.matrix() # matrix w.r.t to the module's default basis + [ 1 -3 1] + [-18 39 -18] + [-25 54 -25] + + """ + from sage.matrix.constructor import matrix + fmodule1 = self.domain() + fmodule2 = self.codomain() + if basis1 is None: + basis1 = fmodule1.default_basis() + elif basis1 not in fmodule1.bases(): + raise TypeError(str(basis1) + " is not a basis on the " + \ + str(fmodule1) + ".") + if basis2 is None: + if self.is_endomorphism(): + basis2 = basis1 + else: + basis2 = fmodule2.default_basis() + elif basis2 not in fmodule2.bases(): + raise TypeError(str(basis2) + " is not a basis on the " + \ + str(fmodule2) + ".") + if (basis1, basis2) not in self._matrices: + if self._is_identity: + # The identity endomorphism + # ------------------------- + if basis1 == basis2: + # the matrix is the identity matrix: + ring = fmodule1.base_ring() + zero = ring.zero() + one = ring.one() + size = fmodule1.rank() + mat = [] + for i in range(size): + row = [zero]*size + row[i] = one + mat.append(row) + else: + # the matrix is the change-of-basis matrix: + change = fmodule1.change_of_basis(basis1, basis2) + mat = [[change[[i,j]] for j in fmodule1.irange()] + for i in fmodule1.irange()] + self._matrices[(basis1, basis2)] = matrix(mat) + else: + # Generic homomorphism + # -------------------- + b1_list = [bases[0] for bases in self._matrices] + b2_list = [bases[1] for bases in self._matrices] + if basis1 in b1_list: + for b2 in b2_list: + if (basis2, b2) in fmodule2._basis_changes: + nb2 = b2 + break + else: + raise ValueError("no start basis could be found for " + + "applying the change-of-basis formula") + change2 = fmodule2._basis_changes[(basis2, nb2)] + mat2 = matrix( [[change2[[i,j]] for j in fmodule2.irange()] + for i in fmodule2.irange()] ) + self._matrices[(basis1, basis2)] = \ + mat2 * self._matrices[(basis1,nb2)] + elif basis2 in b2_list: + for b1 in b1_list: + if (b1, basis1) in fmodule1._basis_changes: + nb1 = b1 + break + else: + raise ValueError("no start basis could be found for " + + "applying the change-of-basis formula") + change1 = fmodule1._basis_changes[(nb1, basis1)] + mat1 = matrix( [[change1[[i,j]] for j in fmodule1.irange()] + for i in fmodule1.irange()] ) + self._matrices[(basis1, basis2)] = \ + self._matrices[(nb1,basis2)] * mat1 + else: # most general change-of-basis formula + for (b1, b2) in self._matrices: + if (b1, basis1) in fmodule1._basis_changes and \ + (basis2, b2) in fmodule2._basis_changes: + nb1, nb2 = b1, b2 + break + else: + raise ValueError("no start basis could be found for " + + "applying the change-of-basis formula") + change1 = fmodule1._basis_changes[(nb1, basis1)] + change2 = fmodule2._basis_changes[(basis2, nb2)] + mat1 = matrix( [[change1[[i,j]] for j in fmodule1.irange()] + for i in fmodule1.irange()] ) + mat2 = matrix( [[change2[[i,j]] for j in fmodule2.irange()] + for i in fmodule2.irange()] ) + self._matrices[(basis1, basis2)] = \ + mat2 * self._matrices[(nb1,nb2)] * mat1 + return self._matrices[(basis1, basis2)] + + def _common_bases(self, other): + r""" + Return a pair of bases in which ``self`` and ``other`` have a known + matrix representation. + + INPUT: + + - ``other`` -- another homomorphism in the same hom-set + + OUTPUT: + + - a pair of bases in which ``self`` and ``other`` have a known + matrix representation. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: N = FiniteRankFreeModule(ZZ, 2, name='N') + sage: e = M.basis('e') ; f = N.basis('f') + sage: phi = M.hom(N, [[-1,2,0], [5,1,2]]) + sage: psi = M.hom(N, [[1,1,0], [4,1,3]]) + sage: phi._common_bases(psi) # matrices of phi and psi both defined on (e,f) + (Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer Ring, + Basis (f_0,f_1) on the Rank-2 free module N over the Integer Ring) + sage: a = M.automorphism() ; a[0,2], a[1,0], a[2,1] = 1, -1, -1 + sage: ep = e.new_basis(a, 'ep', latex_symbol="e'") + sage: psi = M.hom(N, [[1,1,0], [4,1,3]], bases=(ep,f)) + sage: phi._common_bases(psi) # matrix of psi w.r.t. (e,f) computed + (Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer Ring, + Basis (f_0,f_1) on the Rank-2 free module N over the Integer Ring) + sage: psi = M.hom(N, [[1,1,0], [4,1,3]], bases=(ep,f)) + sage: psi._common_bases(phi) # matrix of phi w.r.t. (ep,f) computed + (Basis (ep_0,ep_1,ep_2) on the Rank-3 free module M over the Integer Ring, + Basis (f_0,f_1) on the Rank-2 free module N over the Integer Ring) + + """ + resu = None + for bases in self._matrices: + try: + other.matrix(*bases) + resu = bases + break + except ValueError: + continue + if resu is None: + for bases in other._matrices: + try: + self.matrix(*bases) + resu = bases + break + except ValueError: + continue + return resu diff --git a/src/sage/tensor/modules/free_module_tensor.py b/src/sage/tensor/modules/free_module_tensor.py new file mode 100644 index 00000000000..96ca6775366 --- /dev/null +++ b/src/sage/tensor/modules/free_module_tensor.py @@ -0,0 +1,3144 @@ +r""" +Tensors on free modules + +The class :class:`FreeModuleTensor` implements tensors on a free module `M` +of finite rank over a commutative ring. A *tensor of type* `(k,l)` on `M` +is a multilinear map: + +.. MATH:: + + \underbrace{M^*\times\cdots\times M^*}_{k\ \; \mbox{times}} + \times \underbrace{M\times\cdots\times M}_{l\ \; \mbox{times}} + \longrightarrow R + +where `R` is the commutative ring over which the free module `M` is defined +and `M^* = \mathrm{Hom}_R(M,R)` is the dual of `M`. The integer `k + l` is +called the *tensor rank*. The set `T^{(k,l)}(M)` of tensors of type `(k,l)` +on `M` is a free module of finite rank over `R`, described by the +class :class:`~sage.tensor.modules.tensor_free_module.TensorFreeModule`. + +Various derived classes of :class:`FreeModuleTensor` are devoted to specific +tensors: + +* :class:`FiniteRankFreeModuleElement` for elements of `M`, considered as + type-(1,0) tensors thanks to the canonical identification `M^{**}=M` (which + holds since `M` is a free module of finite rank); + +* :class:`~sage.tensor.modules.free_module_alt_form.FreeModuleAltForm` for + fully antisymmetric type-`(0, l)` tensors (alternating forms); + +* :class:`~sage.tensor.modules.free_module_automorphism.FreeModuleAutomorphism` + for type-(1,1) tensors representing invertible endomorphisms. + +Each of these classes is a Sage *element* class, the corresponding *parent* +classes being: + +* for :class:`FreeModuleTensor`: + :class:`~sage.tensor.modules.tensor_free_module.TensorFreeModule` +* for :class:`FiniteRankFreeModuleElement`: + :class:`~sage.tensor.modules.finite_rank_free_module.FiniteRankFreeModule` +* for :class:`~sage.tensor.modules.free_module_alt_form.FreeModuleAltForm`: + :class:`~sage.tensor.modules.ext_pow_free_module.ExtPowerFreeModule` +* for + :class:`~sage.tensor.modules.free_module_automorphism.FreeModuleAutomorphism`: + :class:`~sage.tensor.modules.free_module_linear_group.FreeModuleLinearGroup` + + +AUTHORS: + +- Eric Gourgoulhon, Michal Bejger (2014-2015): initial version + +REFERENCES: + +- Chap. 21 of R. Godement: *Algebra*, Hermann (Paris) / Houghton Mifflin + (Boston) (1968) +- Chap. 12 of J. M. Lee: *Introduction to Smooth Manifolds*, 2nd ed., Springer + (New York) (2013) (only when the free module is a vector space) +- Chap. 2 of B. O'Neill: *Semi-Riemannian Geometry*, Academic Press (San Diego) + (1983) + +EXAMPLES: + +A tensor of type `(1, 1)` on a rank-3 free module over `\ZZ`:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: t = M.tensor((1,1), name='t') ; t + Type-(1,1) tensor t on the Rank-3 free module M over the Integer Ring + sage: t.parent() + Free module of type-(1,1) tensors on the Rank-3 free module M + over the Integer Ring + sage: t.parent() is M.tensor_module(1,1) + True + sage: t in M.tensor_module(1,1) + True + +Setting some component of the tensor in a given basis:: + + sage: e = M.basis('e') ; e + Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer Ring + sage: t.set_comp(e)[0,0] = -3 # the component [0,0] w.r.t. basis e is set to -3 + +The unset components are assumed to be zero:: + + sage: t.comp(e)[:] # list of all components w.r.t. basis e + [-3 0 0] + [ 0 0 0] + [ 0 0 0] + sage: t.display(e) # displays the expansion of t on the basis e_i*e^j of T^(1,1)(M) + t = -3 e_0*e^0 + +The commands ``t.set_comp(e)`` and ``t.comp(e)`` can be abridged by providing +the basis as the first argument in the square brackets:: + + sage: t[e,0,0] = -3 + sage: t[e,:] + [-3 0 0] + [ 0 0 0] + [ 0 0 0] + +Actually, since ``e`` is ``M``'s default basis, the mention of ``e`` +can be omitted:: + + sage: t[0,0] = -3 + sage: t[:] + [-3 0 0] + [ 0 0 0] + [ 0 0 0] + +For tensors of rank 2, the matrix of components w.r.t. a given basis is +obtained via the function ``matrix``:: + + sage: matrix(t.comp(e)) + [-3 0 0] + [ 0 0 0] + [ 0 0 0] + sage: matrix(t.comp(e)).parent() + Full MatrixSpace of 3 by 3 dense matrices over Integer Ring + +Tensor components can be modified (reset) at any time:: + + sage: t[0,0] = 0 + sage: t[:] + [0 0 0] + [0 0 0] + [0 0 0] + +Checking that ``t`` is zero:: + + sage: t.is_zero() + True + sage: t == 0 + True + sage: t == M.tensor_module(1,1).zero() # the zero element of the module of all type-(1,1) tensors on M + True + +The components are managed by the class +:class:`~sage.tensor.modules.comp.Components`:: + + sage: type(t.comp(e)) + + +Only non-zero components are actually stored, in the dictionary :attr:`_comp` +of class :class:`~sage.tensor.modules.comp.Components`, whose keys are +the indices:: + + sage: t.comp(e)._comp + {} + sage: t.set_comp(e)[0,0] = -3 ; t.set_comp(e)[1,2] = 2 + sage: t.comp(e)._comp # random output order (dictionary) + {(0, 0): -3, (1, 2): 2} + sage: t.display(e) + t = -3 e_0*e^0 + 2 e_1*e^2 + +Further tests of the comparison operator:: + + sage: t.is_zero() + False + sage: t == 0 + False + sage: t == M.tensor_module(1,1).zero() + False + sage: t1 = t.copy() + sage: t1 == t + True + sage: t1[2,0] = 4 + sage: t1 == t + False + +As a multilinear map `M^* \times M \rightarrow \ZZ`, the type-`(1,1)` +tensor ``t`` acts on pairs formed by a linear form and a module element:: + + sage: a = M.linear_form(name='a') ; a[:] = (2, 1, -3) ; a + Linear form a on the Rank-3 free module M over the Integer Ring + sage: b = M([1,-6,2], name='b') ; b + Element b of the Rank-3 free module M over the Integer Ring + sage: t(a,b) + -2 + +""" +#****************************************************************************** +# Copyright (C) 2015 Eric Gourgoulhon +# Copyright (C) 2015 Michal Bejger +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# http://www.gnu.org/licenses/ +#****************************************************************************** + +from sage.rings.integer import Integer +from sage.structure.element import ModuleElement +from sage.tensor.modules.comp import (Components, CompWithSym, CompFullySym, + CompFullyAntiSym) +from sage.tensor.modules.tensor_with_indices import TensorWithIndices + +class FreeModuleTensor(ModuleElement): + r""" + Tensor over a free module of finite rank over a commutative ring. + + This is a Sage *element* class, the corresponding *parent* class being + :class:`~sage.tensor.modules.tensor_free_module.TensorFreeModule`. + + INPUT: + + - ``fmodule`` -- free module `M` of finite rank over a commutative ring + `R`, as an instance of + :class:`~sage.tensor.modules.finite_rank_free_module.FiniteRankFreeModule` + - ``tensor_type`` -- pair ``(k, l)`` with ``k`` being the contravariant + rank and ``l`` the covariant rank + - ``name`` -- (default: ``None``) name given to the tensor + - ``latex_name`` -- (default: ``None``) LaTeX symbol to denote the tensor; + if none is provided, the LaTeX symbol is set to ``name`` + - ``sym`` -- (default: ``None``) a symmetry or a list of symmetries among + the tensor arguments: each symmetry is described by a tuple containing + the positions of the involved arguments, with the convention + ``position=0`` for the first argument. For instance: + + * ``sym = (0,1)`` for a symmetry between the 1st and 2nd arguments; + * ``sym = [(0,2), (1,3,4)]`` for a symmetry between the 1st and 3rd + arguments and a symmetry between the 2nd, 4th and 5th arguments. + + - ``antisym`` -- (default: ``None``) antisymmetry or list of antisymmetries + among the arguments, with the same convention as for ``sym`` + - ``parent`` -- (default: ``None``) some specific parent (e.g. exterior + power for alternating forms); if ``None``, ``fmodule.tensor_module(k,l)`` + is used + + EXAMPLES: + + A tensor of type `(1,1)` on a rank-3 free module over `\ZZ`:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: t = M.tensor((1,1), name='t') ; t + Type-(1,1) tensor t on the Rank-3 free module M over the Integer Ring + + Tensors are *Element* objects whose parents are tensor free modules:: + + sage: t.parent() + Free module of type-(1,1) tensors on the + Rank-3 free module M over the Integer Ring + sage: t.parent() is M.tensor_module(1,1) + True + + """ + def __init__(self, fmodule, tensor_type, name=None, latex_name=None, + sym=None, antisym=None, parent=None): + r""" + TESTS:: + + sage: from sage.tensor.modules.free_module_tensor import FreeModuleTensor + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: t = FreeModuleTensor(M, (2,1), name='t', latex_name=r'\tau', sym=(0,1)) + sage: t[e,0,0,0] = -3 + sage: TestSuite(t).run(skip="_test_category") # see below + + In the above test suite, _test_category fails because t is not an + instance of t.parent().category().element_class. Actually tensors + must be constructed via TensorFreeModule.element_class and + not by a direct call to FreeModuleTensor:: + + sage: t1 = M.tensor_module(2,1).element_class(M, (2,1), name='t', + ....: latex_name=r'\tau', + ....: sym=(0,1)) + sage: t1[e,0,0,0] = -3 + sage: TestSuite(t1).run() + + """ + if parent is None: + parent = fmodule.tensor_module(*tensor_type) + ModuleElement.__init__(self, parent) + self._fmodule = fmodule + self._tensor_type = tuple(tensor_type) + self._tensor_rank = self._tensor_type[0] + self._tensor_type[1] + self._name = name + if latex_name is None: + self._latex_name = self._name + else: + self._latex_name = latex_name + self._components = {} # dict. of the sets of components on various + # bases, with the bases as keys (initially empty) + + # Treatment of symmetry declarations: + self._sym = [] + if sym is not None and sym != []: + if isinstance(sym[0], (int, Integer)): + # a single symmetry is provided as a tuple -> 1-item list: + sym = [tuple(sym)] + for isym in sym: + if len(isym) > 1: + for i in isym: + if i<0 or i>self._tensor_rank-1: + raise IndexError("invalid position: " + str(i) + + " not in [0," + str(self._tensor_rank-1) + "]") + self._sym.append(tuple(isym)) + self._antisym = [] + if antisym is not None and antisym != []: + if isinstance(antisym[0], (int, Integer)): + # a single antisymmetry is provided as a tuple -> 1-item list: + antisym = [tuple(antisym)] + for isym in antisym: + if len(isym) > 1: + for i in isym: + if i<0 or i>self._tensor_rank-1: + raise IndexError("invalid position: " + str(i) + + " not in [0," + str(self._tensor_rank-1) + "]") + self._antisym.append(tuple(isym)) + + # Final consistency check: + index_list = [] + for isym in self._sym: + index_list += isym + for isym in self._antisym: + index_list += isym + if len(index_list) != len(set(index_list)): + # There is a repeated index position: + raise IndexError("incompatible lists of symmetries: the same " + + "position appears more than once") + + # Initialization of derived quantities: + FreeModuleTensor._init_derived(self) + + ####### Required methods for ModuleElement (beside arithmetic) ####### + + def __nonzero__(self): + r""" + Return ``True`` if ``self`` is nonzero and ``False`` otherwise. + + This method is called by ``self.is_zero()``. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: t = M.tensor((2,1)) + sage: t.add_comp(e) + 3-indices components w.r.t. Basis (e_0,e_1,e_2) on the + Rank-3 free module M over the Integer Ring + sage: t.__nonzero__() # unitialized components are zero + False + sage: t == 0 + True + sage: t[e,1,0,2] = 4 # setting a non-zero component in basis e + sage: t.display() + 4 e_1*e_0*e^2 + sage: t.__nonzero__() + True + sage: t == 0 + False + sage: t[e,1,0,2] = 0 + sage: t.display() + 0 + sage: t.__nonzero__() + False + sage: t == 0 + True + + """ + basis = self.pick_a_basis() + return not self._components[basis].is_zero() + + ##### End of required methods for ModuleElement (beside arithmetic) ##### + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLE:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: t = M.tensor((2,1), name='t') + sage: t + Type-(2,1) tensor t on the Rank-3 free module M over the Integer Ring + + """ + # Special cases + if self._tensor_type == (0,2) and self._sym == [(0,1)]: + description = "Symmetric bilinear form " + else: + # Generic case + description = "Type-({},{}) tensor".format( + self._tensor_type[0], self._tensor_type[1]) + if self._name is not None: + description += " " + self._name + description += " on the {}".format(self._fmodule) + return description + + def _latex_(self): + r""" + LaTeX representation of the object. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: t = M.tensor((2,1), name='t') + sage: t._latex_() + 't' + sage: latex(t) + t + sage: t = M.tensor((2,1), name='t', latex_name=r'\tau') + sage: t._latex_() + '\\tau' + sage: latex(t) + \tau + sage: t = M.tensor((2,1)) # unnamed tensor + sage: t._latex_() + '\\mbox{Type-(2,1) tensor on the Rank-3 free module M over the Integer Ring}' + + """ + if self._latex_name is None: + return r'\mbox{' + str(self) + r'}' + return self._latex_name + + def _init_derived(self): + r""" + Initialize the derived quantities + + EXAMPLE:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: t = M.tensor((2,1), name='t') + sage: t._init_derived() + + """ + pass # no derived quantities + + def _del_derived(self): + r""" + Delete the derived quantities + + EXAMPLE:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: t = M.tensor((2,1), name='t') + sage: t._del_derived() + + """ + pass # no derived quantities + + #### Simple accessors #### + + def tensor_type(self): + r""" + Return the tensor type of ``self``. + + OUTPUT: + + - pair ``(k, l)``, where ``k`` is the contravariant rank and ``l`` + is the covariant rank + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3) + sage: M.an_element().tensor_type() + (1, 0) + sage: t = M.tensor((2,1)) + sage: t.tensor_type() + (2, 1) + + """ + return self._tensor_type + + def tensor_rank(self): + r""" + Return the tensor rank of ``self``. + + OUTPUT: + + - integer ``k+l``, where ``k`` is the contravariant rank and ``l`` + is the covariant rank + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3) + sage: M.an_element().tensor_rank() + 1 + sage: t = M.tensor((2,1)) + sage: t.tensor_rank() + 3 + + """ + return self._tensor_rank + + def base_module(self): + r""" + Return the module on which ``self`` is defined. + + OUTPUT: + + - instance of :class:`FiniteRankFreeModule` representing the free + module on which the tensor is defined. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: M.an_element().base_module() + Rank-3 free module M over the Integer Ring + sage: t = M.tensor((2,1)) + sage: t.base_module() + Rank-3 free module M over the Integer Ring + sage: t.base_module() is M + True + + """ + return self._fmodule + + def symmetries(self): + r""" + Print the list of symmetries and antisymmetries of ``self``. + + EXAMPLES: + + Various symmetries / antisymmetries for a rank-4 tensor:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: t = M.tensor((4,0), name='T') # no symmetry declared + sage: t.symmetries() + no symmetry; no antisymmetry + sage: t = M.tensor((4,0), name='T', sym=(0,1)) + sage: t.symmetries() + symmetry: (0, 1); no antisymmetry + sage: t = M.tensor((4,0), name='T', sym=[(0,1), (2,3)]) + sage: t.symmetries() + symmetries: [(0, 1), (2, 3)]; no antisymmetry + sage: t = M.tensor((4,0), name='T', sym=(0,1), antisym=(2,3)) + sage: t.symmetries() + symmetry: (0, 1); antisymmetry: (2, 3) + + """ + if len(self._sym) == 0: + s = "no symmetry; " + elif len(self._sym) == 1: + s = "symmetry: {}; ".format(self._sym[0]) + else: + s = "symmetries: {}; ".format(self._sym) + if len(self._antisym) == 0: + a = "no antisymmetry" + elif len(self._antisym) == 1: + a = "antisymmetry: {}".format(self._antisym[0]) + else: + a = "antisymmetries: {}".format(self._antisym) + print(s+a) + + #### End of simple accessors ##### + + def display(self, basis=None, format_spec=None): + r""" + Display ``self`` in terms of its expansion w.r.t. a given module basis. + + The expansion is actually performed onto tensor products of elements + of the given basis and of elements of its dual basis (see examples + below). + The output is either text-formatted (console mode) or LaTeX-formatted + (notebook mode). + + INPUT: + + - ``basis`` -- (default: ``None``) basis of the free module with + respect to which the tensor is expanded; if none is provided, + the module's default basis is assumed + - ``format_spec`` -- (default: ``None``) format specification passed + to ``self._fmodule._output_formatter`` to format the output + + EXAMPLES: + + Display of a module element (type-`(1,0)` tensor):: + + sage: M = FiniteRankFreeModule(QQ, 2, name='M', start_index=1) + sage: e = M.basis('e') ; e + Basis (e_1,e_2) on the 2-dimensional vector space M over the + Rational Field + sage: v = M([1/3,-2], name='v') + sage: v.display(e) + v = 1/3 e_1 - 2 e_2 + sage: v.display() # a shortcut since e is M's default basis + v = 1/3 e_1 - 2 e_2 + sage: latex(v.display()) # display in the notebook + v = \frac{1}{3} e_1 -2 e_2 + + A shortcut is ``disp()``:: + + sage: v.disp() + v = 1/3 e_1 - 2 e_2 + + Display of a linear form (type-`(0,1)` tensor):: + + sage: de = e.dual_basis() ; de + Dual basis (e^1,e^2) on the 2-dimensional vector space M over the + Rational Field + sage: w = - 3/4 * de[1] + de[2] ; w + Linear form on the 2-dimensional vector space M over the Rational + Field + sage: w.set_name('w', latex_name='\omega') + sage: w.display() + w = -3/4 e^1 + e^2 + sage: latex(w.display()) # display in the notebook + \omega = -\frac{3}{4} e^1 +e^2 + + Display of a type-`(1,1)` tensor:: + + sage: t = v*w ; t # the type-(1,1) is formed as the tensor product of v by w + Type-(1,1) tensor v*w on the 2-dimensional vector space M over the + Rational Field + sage: t.display() + v*w = -1/4 e_1*e^1 + 1/3 e_1*e^2 + 3/2 e_2*e^1 - 2 e_2*e^2 + sage: latex(t.display()) # display in the notebook + v\otimes \omega = -\frac{1}{4} e_1\otimes e^1 + + \frac{1}{3} e_1\otimes e^2 + \frac{3}{2} e_2\otimes e^1 + -2 e_2\otimes e^2 + + Display in a basis which is not the default one:: + + sage: a = M.automorphism(matrix=[[1,2],[3,4]], basis=e) + sage: f = e.new_basis(a, 'f') + sage: v.display(f) # the components w.r.t basis f are first computed via the change-of-basis formula defined by a + v = -8/3 f_1 + 3/2 f_2 + sage: w.display(f) + w = 9/4 f^1 + 5/2 f^2 + sage: t.display(f) + v*w = -6 f_1*f^1 - 20/3 f_1*f^2 + 27/8 f_2*f^1 + 15/4 f_2*f^2 + + The output format can be set via the argument ``output_formatter`` + passed at the module construction:: + + sage: N = FiniteRankFreeModule(QQ, 2, name='N', start_index=1, + ....: output_formatter=Rational.numerical_approx) + sage: e = N.basis('e') + sage: v = N([1/3,-2], name='v') + sage: v.display() # default format (53 bits of precision) + v = 0.333333333333333 e_1 - 2.00000000000000 e_2 + sage: latex(v.display()) + v = 0.333333333333333 e_1 -2.00000000000000 e_2 + + The output format is then controled by the argument ``format_spec`` of + the method :meth:`display`:: + + sage: v.display(format_spec=10) # 10 bits of precision + v = 0.33 e_1 - 2.0 e_2 + + """ + from sage.misc.latex import latex + from sage.tensor.modules.format_utilities import is_atomic, \ + FormattedExpansion + if basis is None: + basis = self._fmodule._def_basis + cobasis = basis.dual_basis() + comp = self.comp(basis) + terms_txt = [] + terms_latex = [] + n_con = self._tensor_type[0] + for ind in comp.index_generator(): + ind_arg = ind + (format_spec,) + coef = comp[ind_arg] + if coef != 0: + bases_txt = [] + bases_latex = [] + for k in range(n_con): + bases_txt.append(basis[ind[k]]._name) + bases_latex.append(latex(basis[ind[k]])) + for k in range(n_con, self._tensor_rank): + bases_txt.append(cobasis[ind[k]]._name) + bases_latex.append(latex(cobasis[ind[k]])) + basis_term_txt = "*".join(bases_txt) + basis_term_latex = r"\otimes ".join(bases_latex) + coef_txt = repr(coef) + if coef_txt == "1": + terms_txt.append(basis_term_txt) + terms_latex.append(basis_term_latex) + elif coef_txt == "-1": + terms_txt.append("-" + basis_term_txt) + terms_latex.append("-" + basis_term_latex) + else: + coef_latex = latex(coef) + if is_atomic(coef_txt): + terms_txt.append(coef_txt + " " + basis_term_txt) + else: + terms_txt.append("(" + coef_txt + ") " + + basis_term_txt) + if is_atomic(coef_latex): + terms_latex.append(coef_latex + basis_term_latex) + else: + terms_latex.append(r"\left(" + coef_latex + + r"\right)" + basis_term_latex) + if terms_txt == []: + expansion_txt = "0" + else: + expansion_txt = terms_txt[0] + for term in terms_txt[1:]: + if term[0] == "-": + expansion_txt += " - " + term[1:] + else: + expansion_txt += " + " + term + if terms_latex == []: + expansion_latex = "0" + else: + expansion_latex = terms_latex[0] + for term in terms_latex[1:]: + if term[0] == "-": + expansion_latex += term + else: + expansion_latex += "+" + term + if self._name is None: + resu_txt = expansion_txt + else: + resu_txt = self._name + " = " + expansion_txt + if self._latex_name is None: + resu_latex = expansion_latex + else: + resu_latex = latex(self) + " = " + expansion_latex + return FormattedExpansion(resu_txt, resu_latex) + + disp = display + + + def view(self, basis=None, format_spec=None): + r""" + Deprecated method. + + Use method :meth:`display` instead. + + EXAMPLE:: + + sage: M = FiniteRankFreeModule(ZZ, 2, 'M') + sage: e = M.basis('e') + sage: v = M([2,-3], basis=e, name='v') + sage: v.view(e) + doctest:...: DeprecationWarning: Use function display() instead. + See http://trac.sagemath.org/15916 for details. + v = 2 e_0 - 3 e_1 + sage: v.display(e) + v = 2 e_0 - 3 e_1 + + """ + from sage.misc.superseded import deprecation + deprecation(15916, 'Use function display() instead.') + return self.display(basis=basis, format_spec=format_spec) + + def set_name(self, name=None, latex_name=None): + r""" + Set (or change) the text name and LaTeX name of ``self``. + + INPUT: + + - ``name`` -- (default: ``None``) string; name given to the tensor + - ``latex_name`` -- (default: ``None``) string; LaTeX symbol to denote + the tensor; if None while ``name`` is provided, the LaTeX symbol + is set to ``name`` + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: t = M.tensor((2,1)) ; t + Type-(2,1) tensor on the Rank-3 free module M over the Integer Ring + sage: t.set_name('t') ; t + Type-(2,1) tensor t on the Rank-3 free module M over the Integer Ring + sage: latex(t) + t + sage: t.set_name(latex_name=r'\tau') ; t + Type-(2,1) tensor t on the Rank-3 free module M over the Integer Ring + sage: latex(t) + \tau + + """ + if name is not None: + self._name = name + if latex_name is None: + self._latex_name = self._name + if latex_name is not None: + self._latex_name = latex_name + + def _new_instance(self): + r""" + Create a tensor of the same tensor type and with the same symmetries + as ``self``. + + EXAMPLE:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: t = M.tensor((2,1), name='t') + sage: t._new_instance() + Type-(2,1) tensor on the Rank-3 free module M over the Integer Ring + sage: t._new_instance().parent() is t.parent() + True + + """ + return self.__class__(self._fmodule, self._tensor_type, sym=self._sym, + antisym=self._antisym) + + def _new_comp(self, basis): + r""" + Create some (uninitialized) components of ``self`` w.r.t a given + module basis. + + This method, to be called by :meth:`comp`, must be redefined by derived + classes to adapt the output to the relevant subclass of + :class:`~sage.tensor.modules.comp.Components`. + + INPUT: + + - ``basis`` -- basis of the free module on which ``self`` is defined + + OUTPUT: + + - an instance of :class:`~sage.tensor.modules.comp.Components` + (or of one of its subclasses) + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: t = M.tensor((2,1), name='t') + sage: e = M.basis('e') + sage: t._new_comp(e) + 3-indices components w.r.t. Basis (e_0,e_1,e_2) on the + Rank-3 free module M over the Integer Ring + sage: a = M.tensor((2,1), name='a', sym=(0,1)) + sage: a._new_comp(e) + 3-indices components w.r.t. Basis (e_0,e_1,e_2) on the + Rank-3 free module M over the Integer Ring, + with symmetry on the index positions (0, 1) + + """ + fmodule = self._fmodule # the base free module + if not self._sym and not self._antisym: + return Components(fmodule._ring, basis, self._tensor_rank, + start_index=fmodule._sindex, + output_formatter=fmodule._output_formatter) + for isym in self._sym: + if len(isym) == self._tensor_rank: + return CompFullySym(fmodule._ring, basis, self._tensor_rank, + start_index=fmodule._sindex, + output_formatter=fmodule._output_formatter) + for isym in self._antisym: + if len(isym) == self._tensor_rank: + return CompFullyAntiSym(fmodule._ring, basis, self._tensor_rank, + start_index=fmodule._sindex, + output_formatter=fmodule._output_formatter) + return CompWithSym(fmodule._ring, basis, self._tensor_rank, + start_index=fmodule._sindex, + output_formatter=fmodule._output_formatter, + sym=self._sym, antisym=self._antisym) + + def components(self, basis=None, from_basis=None): + r""" + Return the components of ``self`` w.r.t to a given module basis. + + If the components are not known already, they are computed by the + tensor change-of-basis formula from components in another basis. + + INPUT: + + - ``basis`` -- (default: ``None``) basis in which the components are + required; if none is provided, the components are assumed to refer + to the module's default basis + - ``from_basis`` -- (default: ``None``) basis from which the + required components are computed, via the tensor change-of-basis + formula, if they are not known already in the basis ``basis``; + if none, a basis from which both the components and a change-of-basis + to ``basis`` are known is selected. + + OUTPUT: + + - components in the basis ``basis``, as an instance of the + class :class:`~sage.tensor.modules.comp.Components` + + EXAMPLES: + + Components of a tensor of type-`(1,1)`:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M', start_index=1) + sage: e = M.basis('e') + sage: t = M.tensor((1,1), name='t') + sage: t[1,2] = -3 ; t[3,3] = 2 + sage: t.components() + 2-indices components w.r.t. Basis (e_1,e_2,e_3) + on the Rank-3 free module M over the Integer Ring + sage: t.components() is t.components(e) # since e is M's default basis + True + sage: t.components()[:] + [ 0 -3 0] + [ 0 0 0] + [ 0 0 2] + + A shortcut is ``t.comp()``:: + + sage: t.comp() is t.components() + True + + A direct access to the components w.r.t. the module's default basis is + provided by the square brackets applied to the tensor itself:: + + sage: t[1,2] is t.comp(e)[1,2] + True + sage: t[:] + [ 0 -3 0] + [ 0 0 0] + [ 0 0 2] + + Components computed via a change-of-basis formula:: + + sage: a = M.automorphism() + sage: a[:] = [[0,0,1], [1,0,0], [0,-1,0]] + sage: f = e.new_basis(a, 'f') + sage: t.comp(f) + 2-indices components w.r.t. Basis (f_1,f_2,f_3) + on the Rank-3 free module M over the Integer Ring + sage: t.comp(f)[:] + [ 0 0 0] + [ 0 2 0] + [-3 0 0] + + """ + fmodule = self._fmodule + if basis is None: + basis = fmodule._def_basis + if basis not in self._components: + # The components must be computed from + # those in the basis from_basis + if from_basis is None: + for known_basis in self._components: + if (known_basis, basis) in self._fmodule._basis_changes \ + and (basis, known_basis) in self._fmodule._basis_changes: + from_basis = known_basis + break + if from_basis is None: + raise ValueError("no basis could be found for computing " + + "the components in the {}".format(basis)) + elif from_basis not in self._components: + raise ValueError("the tensor components are not known in " + + "the {}".format(from_basis)) + (n_con, n_cov) = self._tensor_type + if n_cov > 0: + if (from_basis, basis) not in fmodule._basis_changes: + raise ValueError("the change-of-basis matrix from the " + + "{} to the {}".format(from_basis, basis) + + " has not been set") + pp = \ + fmodule._basis_changes[(from_basis, basis)].comp(from_basis) + # pp not used if n_cov = 0 (pure contravariant tensor) + if n_con > 0: + if (basis, from_basis) not in fmodule._basis_changes: + raise ValueError("the change-of-basis matrix from the " + + "{} to the {}".format(basis, from_basis) + + " has not been set") + ppinv = \ + fmodule._basis_changes[(basis, from_basis)].comp(from_basis) + # ppinv not used if n_con = 0 (pure covariant tensor) + old_comp = self._components[from_basis] + new_comp = self._new_comp(basis) + rank = self._tensor_rank + # loop on the new components: + for ind_new in new_comp.non_redundant_index_generator(): + # Summation on the old components multiplied by the proper + # change-of-basis matrix elements (tensor formula): + res = 0 + for ind_old in old_comp.index_generator(): + t = old_comp[[ind_old]] + for i in range(n_con): # loop on contravariant indices + t *= ppinv[[ind_new[i], ind_old[i]]] + for i in range(n_con,rank): # loop on covariant indices + t *= pp[[ind_old[i], ind_new[i]]] + res += t + new_comp[ind_new] = res + self._components[basis] = new_comp + # end of case where the computation was necessary + return self._components[basis] + + comp = components + + def set_comp(self, basis=None): + r""" + Return the components of ``self`` w.r.t. a given module basis for + assignment. + + The components with respect to other bases are deleted, in order to + avoid any inconsistency. To keep them, use the method :meth:`add_comp` + instead. + + INPUT: + + - ``basis`` -- (default: ``None``) basis in which the components are + defined; if none is provided, the components are assumed to refer to + the module's default basis + + OUTPUT: + + - components in the given basis, as an instance of the + class :class:`~sage.tensor.modules.comp.Components`; if such + components did not exist previously, they are created. + + EXAMPLES: + + Setting components of a type-`(1,1)` tensor:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: t = M.tensor((1,1), name='t') + sage: t.set_comp()[0,1] = -3 + sage: t.display() + t = -3 e_0*e^1 + sage: t.set_comp()[1,2] = 2 + sage: t.display() + t = -3 e_0*e^1 + 2 e_1*e^2 + sage: t.set_comp(e) + 2-indices components w.r.t. Basis (e_0,e_1,e_2) on the + Rank-3 free module M over the Integer Ring + + Setting components in a new basis:: + + sage: f = M.basis('f') + sage: t.set_comp(f)[0,1] = 4 + sage: t._components.keys() # the components w.r.t. basis e have been deleted + [Basis (f_0,f_1,f_2) on the Rank-3 free module M over the Integer Ring] + sage: t.display(f) + t = 4 f_0*f^1 + + The components w.r.t. basis e can be deduced from those w.r.t. basis f, + once a relation between the two bases has been set:: + + sage: a = M.automorphism() + sage: a[:] = [[0,0,1], [1,0,0], [0,-1,0]] + sage: M.set_change_of_basis(e, f, a) + sage: t.display(e) + t = -4 e_1*e^2 + sage: t._components.keys() # random output (dictionary keys) + [Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer Ring, + Basis (f_0,f_1,f_2) on the Rank-3 free module M over the Integer Ring] + + """ + if basis is None: + basis = self._fmodule._def_basis + if basis not in self._components: + if basis not in self._fmodule._known_bases: + raise ValueError("the {} has not been ".format(basis) + + "defined on the {}".format(self._fmodule)) + self._components[basis] = self._new_comp(basis) + self._del_derived() # deletes the derived quantities + self.del_other_comp(basis) + return self._components[basis] + + def add_comp(self, basis=None): + r""" + Return the components of ``self`` w.r.t. a given module basis for + assignment, keeping the components w.r.t. other bases. + + To delete the components w.r.t. other bases, use the method + :meth:`set_comp` instead. + + INPUT: + + - ``basis`` -- (default: ``None``) basis in which the components are + defined; if none is provided, the components are assumed to refer to + the module's default basis + + .. WARNING:: + + If the tensor has already components in other bases, it + is the user's responsability to make sure that the components + to be added are consistent with them. + + OUTPUT: + + - components in the given basis, as an instance of the + class :class:`~sage.tensor.modules.comp.Components`; + if such components did not exist previously, they are created + + EXAMPLES: + + Setting components of a type-(1,1) tensor:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: t = M.tensor((1,1), name='t') + sage: t.add_comp()[0,1] = -3 + sage: t.display() + t = -3 e_0*e^1 + sage: t.add_comp()[1,2] = 2 + sage: t.display() + t = -3 e_0*e^1 + 2 e_1*e^2 + sage: t.add_comp(e) + 2-indices components w.r.t. Basis (e_0,e_1,e_2) on the + Rank-3 free module M over the Integer Ring + + Adding components in a new basis:: + + sage: f = M.basis('f') + sage: t.add_comp(f)[0,1] = 4 + + The components w.r.t. basis e have been kept:: + + sage: t._components.keys() # # random output (dictionary keys) + [Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer Ring, + Basis (f_0,f_1,f_2) on the Rank-3 free module M over the Integer Ring] + sage: t.display(f) + t = 4 f_0*f^1 + sage: t.display(e) + t = -3 e_0*e^1 + 2 e_1*e^2 + + """ + if basis is None: basis = self._fmodule._def_basis + if basis not in self._components: + if basis not in self._fmodule._known_bases: + raise ValueError("the {} has not been ".format(basis) + + "defined on the {}".format(self._fmodule)) + self._components[basis] = self._new_comp(basis) + self._del_derived() # deletes the derived quantities + return self._components[basis] + + + def del_other_comp(self, basis=None): + r""" + Delete all the components but those corresponding to ``basis``. + + INPUT: + + - ``basis`` -- (default: ``None``) basis in which the components are + kept; if none the module's default basis is assumed + + EXAMPLE: + + Deleting components of a module element:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M', start_index=1) + sage: e = M.basis('e') + sage: u = M([2,1,-5]) + sage: f = M.basis('f') + sage: u.add_comp(f)[:] = [0,4,2] + sage: u._components.keys() # random output (dictionary keys) + [Basis (e_1,e_2,e_3) on the Rank-3 free module M over the Integer Ring, + Basis (f_1,f_2,f_3) on the Rank-3 free module M over the Integer Ring] + sage: u.del_other_comp(f) + sage: u._components.keys() + [Basis (f_1,f_2,f_3) on the Rank-3 free module M over the Integer Ring] + + Let us restore the components w.r.t. e and delete those w.r.t. f:: + + sage: u.add_comp(e)[:] = [2,1,-5] + sage: u._components.keys() # random output (dictionary keys) + [Basis (e_1,e_2,e_3) on the Rank-3 free module M over the Integer Ring, + Basis (f_1,f_2,f_3) on the Rank-3 free module M over the Integer Ring] + sage: u.del_other_comp() # default argument: basis = e + sage: u._components.keys() + [Basis (e_1,e_2,e_3) on the Rank-3 free module M over the Integer Ring] + + """ + if basis is None: basis = self._fmodule._def_basis + if basis not in self._components: + raise ValueError("the components w.r.t. the {}".format(basis) + + " have not been defined") + to_be_deleted = [] + for other_basis in self._components: + if other_basis != basis: + to_be_deleted.append(other_basis) + for other_basis in to_be_deleted: + del self._components[other_basis] + + def __getitem__(self, args): + r""" + Return a component w.r.t. some basis. + + NB: if ``args`` is a string, this method acts as a shortcut for + tensor contractions and symmetrizations, the string containing + abstract indices. + + INPUT: + + - ``args`` -- list of indices defining the component; if ``[:]`` is + provided, all the components are returned. The basis can be passed + as the first item of ``args``; if not, the free module's default + basis is assumed. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: t = M.tensor((2,1), name='t') + sage: e = M.basis('e') + sage: t.add_comp(e) + 3-indices components w.r.t. Basis (e_0,e_1,e_2) on the + Rank-3 free module M over the Integer Ring + sage: t.__getitem__((1,2,0)) # uninitialized components are zero + 0 + sage: t.__getitem__((e,1,2,0)) # same as above since e in the default basis + 0 + sage: t[1,2,0] = -4 + sage: t.__getitem__((e,1,2,0)) + -4 + sage: v = M([3,-5,2]) + sage: v.__getitem__(slice(None)) + [3, -5, 2] + sage: v.__getitem__(slice(None)) == v[:] + True + sage: v.__getitem__((e, slice(None))) + [3, -5, 2] + + """ + if isinstance(args, str): # tensor with specified indices + return TensorWithIndices(self, args).update() + if isinstance(args, list): # case of [[...]] syntax + if isinstance(args[0], (int, Integer, slice)): + basis = self._fmodule._def_basis + else: + basis = args[0] + args = args[1:] + else: + if isinstance(args, (int, Integer, slice)): + basis = self._fmodule._def_basis + elif not isinstance(args[0], (int, Integer, slice)): + basis = args[0] + args = args[1:] + if len(args) == 1: + args = args[0] # to accommodate for [e,:] syntax + else: + basis = self._fmodule._def_basis + return self.comp(basis)[args] + + + def __setitem__(self, args, value): + r""" + Set a component w.r.t. some basis. + + INPUT: + + - ``args`` -- list of indices defining the component; if ``[:]`` is + provided, all the components are set. The basis can be passed + as the first item of ``args``; if not, the free module's default + basis is assumed + - ``value`` -- the value to be set or a list of values if + ``args = [:]`` + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: t = M.tensor((0,2), name='t') + sage: e = M.basis('e') + sage: t.__setitem__((e,0,1), 5) + sage: t.display() + t = 5 e^0*e^1 + sage: t.__setitem__((0,1), 5) # equivalent to above since e is the default basis + sage: t.display() + t = 5 e^0*e^1 + sage: t[0,1] = 5 # end-user usage + sage: t.display() + t = 5 e^0*e^1 + sage: t.__setitem__(slice(None), [[1,-2,3], [-4,5,-6], [7,-8,9]]) + sage: t[:] + [ 1 -2 3] + [-4 5 -6] + [ 7 -8 9] + + """ + if isinstance(args, list): # case of [[...]] syntax + if isinstance(args[0], (int, Integer, slice, tuple)): + basis = self._fmodule._def_basis + else: + basis = args[0] + args = args[1:] + else: + if isinstance(args, (int, Integer, slice)): + basis = self._fmodule._def_basis + elif not isinstance(args[0], (int, Integer, slice)): + basis = args[0] + args = args[1:] + if len(args)==1: + args = args[0] # to accommodate for [e,:] syntax + else: + basis = self._fmodule._def_basis + self.set_comp(basis)[args] = value + + + def copy(self): + r""" + Return an exact copy of ``self``. + + The name and the derived quantities are not copied. + + EXAMPLES: + + Copy of a tensor of type `(1,1)`:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M', start_index=1) + sage: e = M.basis('e') + sage: t = M.tensor((1,1), name='t') + sage: t[1,2] = -3 ; t[3,3] = 2 + sage: t1 = t.copy() + sage: t1[:] + [ 0 -3 0] + [ 0 0 0] + [ 0 0 2] + sage: t1 == t + True + + If the original tensor is modified, the copy is not:: + + sage: t[2,2] = 4 + sage: t1[:] + [ 0 -3 0] + [ 0 0 0] + [ 0 0 2] + sage: t1 == t + False + + """ + resu = self._new_instance() + for basis, comp in self._components.iteritems(): + resu._components[basis] = comp.copy() + return resu + + def common_basis(self, other): + r""" + Find a common basis for the components of ``self`` and ``other``. + + In case of multiple common bases, the free module's default basis is + privileged. If the current components of ``self`` and ``other`` + are all relative to different bases, a common basis is searched + by performing a component transformation, via the transformations + listed in ``self._fmodule._basis_changes``, still privileging + transformations to the free module's default basis. + + INPUT: + + - ``other`` -- a tensor (instance of :class:`FreeModuleTensor`) + + OUPUT: + + - instance of + :class:`~sage.tensor.modules.free_module_basis.FreeModuleBasis` + representing the common basis; if no common basis is found, ``None`` + is returned + + EXAMPLES: + + Common basis for the components of two module elements:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M', start_index=1) + sage: e = M.basis('e') + sage: u = M([2,1,-5]) + sage: f = M.basis('f') + sage: M._basis_changes.clear() # to ensure that bases e and f are unrelated at this stage + sage: v = M([0,4,2], basis=f) + sage: u.common_basis(v) + + The above result is ``None`` since ``u`` and ``v`` have been defined + on different bases and no connection between these bases have + been set:: + + sage: u._components.keys() + [Basis (e_1,e_2,e_3) on the Rank-3 free module M over the Integer Ring] + sage: v._components.keys() + [Basis (f_1,f_2,f_3) on the Rank-3 free module M over the Integer Ring] + + Linking bases ``e`` and ``f`` changes the result:: + + sage: a = M.automorphism() + sage: a[:] = [[0,0,1], [1,0,0], [0,-1,0]] + sage: M.set_change_of_basis(e, f, a) + sage: u.common_basis(v) + Basis (e_1,e_2,e_3) on the Rank-3 free module M over the Integer Ring + + Indeed, v is now known in basis e:: + + sage: v._components.keys() # random output (dictionary keys) + [Basis (f_1,f_2,f_3) on the Rank-3 free module M over the Integer Ring, + Basis (e_1,e_2,e_3) on the Rank-3 free module M over the Integer Ring] + + """ + # Compatibility checks: + if not isinstance(other, FreeModuleTensor): + raise TypeError("the argument must be a tensor on a free module") + fmodule = self._fmodule + if other._fmodule != fmodule: + raise TypeError("the two tensors are not defined on the same " + + "free module") + def_basis = fmodule._def_basis + + # 1/ Search for a common basis among the existing components, i.e. + # without performing any component transformation. + # ------------------------------------------------------------- + if def_basis in self._components and def_basis in other._components: + return def_basis # the module's default basis is privileged + for basis1 in self._components: + if basis1 in other._components: + return basis1 + + # 2/ Search for a common basis via one component transformation + # ---------------------------------------------------------- + # If this point is reached, it is indeed necessary to perform at least + # one component transformation to get a common basis + if def_basis in self._components: + for obasis in other._components: + if (obasis, def_basis) in fmodule._basis_changes: + other.comp(def_basis, from_basis=obasis) + return def_basis + if def_basis in other._components: + for sbasis in self._components: + if (sbasis, def_basis) in fmodule._basis_changes: + self.comp(def_basis, from_basis=sbasis) + return def_basis + # If this point is reached, then def_basis cannot be a common basis + # via a single component transformation + for sbasis in self._components: + for obasis in other._components: + if (obasis, sbasis) in fmodule._basis_changes: + other.comp(sbasis, from_basis=obasis) + return sbasis + if (sbasis, obasis) in fmodule._basis_changes: + self.comp(obasis, from_basis=sbasis) + return obasis + + # 3/ Search for a common basis via two component transformations + # ----------------------------------------------------------- + # If this point is reached, it is indeed necessary to perform at two + # component transformation to get a common basis + for sbasis in self._components: + for obasis in other._components: + if (sbasis, def_basis) in fmodule._basis_changes and \ + (obasis, def_basis) in fmodule._basis_changes: + self.comp(def_basis, from_basis=sbasis) + other.comp(def_basis, from_basis=obasis) + return def_basis + for basis in fmodule._known_bases: + if (sbasis, basis) in fmodule._basis_changes and \ + (obasis, basis) in fmodule._basis_changes: + self.comp(basis, from_basis=sbasis) + other.comp(basis, from_basis=obasis) + return basis + + # If this point is reached, no common basis could be found, even at + # the price of component transformations: + return None + + def pick_a_basis(self): + r""" + Return a basis in which the tensor components are defined. + + The free module's default basis is privileged. + + OUTPUT: + + - instance of + :class:`~sage.tensor.modules.free_module_basis.FreeModuleBasis` + representing the basis + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: t = M.tensor((2,0), name='t') + sage: e = M.basis('e') + sage: t[0,1] = 4 # component set in the default basis (e) + sage: t.pick_a_basis() + Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer Ring + sage: f = M.basis('f') + sage: t.add_comp(f)[2,1] = -4 # the components in basis e are not erased + sage: t.pick_a_basis() + Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer Ring + sage: t.set_comp(f)[2,1] = -4 # the components in basis e not erased + sage: t.pick_a_basis() + Basis (f_0,f_1,f_2) on the Rank-3 free module M over the Integer Ring + + """ + if self._fmodule._def_basis in self._components: + return self._fmodule._def_basis # the default basis is privileged + else: + # a basis is picked arbitrarily: + return self._components.items()[0][0] + + def __eq__(self, other): + r""" + Comparison (equality) operator. + + INPUT: + + - ``other`` -- a tensor or 0 + + OUTPUT: + + - ``True`` if ``self`` is equal to ``other`` and ``False`` otherwise + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: t = M.tensor((2,0), name='t') + sage: e = M.basis('e') + sage: t[0,1] = 7 + sage: t.__eq__(0) + False + sage: t[0,1] = 0 + sage: t.__eq__(0) + True + sage: a = M.tensor((0,2), name='a') + sage: a[0,1] = 7 + sage: t[0,1] = 7 + sage: a[:], t[:] + ( + [0 7 0] [0 7 0] + [0 0 0] [0 0 0] + [0 0 0], [0 0 0] + ) + sage: t.__eq__(a) # False since t and a do not have the same tensor type + False + sage: a = M.tensor((2,0), name='a') # same tensor type as t + sage: a[0,1] = 7 + sage: t.__eq__(a) + True + + """ + if self is other: + return True + + if self._tensor_rank == 0: + raise NotImplementedError("scalar comparison not implemented") + if isinstance(other, (int, Integer)): # other should be 0 + if other == 0: + return self.is_zero() + else: + return False + elif not isinstance(other, FreeModuleTensor): + return False + else: # other is another tensor + if other._fmodule != self._fmodule: + return False + if other._tensor_type != self._tensor_type: + return False + basis = self.common_basis(other) + if basis is None: + raise ValueError("no common basis for the comparison") + return bool(self._components[basis] == other._components[basis]) + + def __ne__(self, other): + r""" + Inequality operator. + + INPUT: + + - ``other`` -- a tensor or 0 + + OUTPUT: + + - ``True`` if ``self`` is different from ``other`` and ``False`` + otherwise + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: t = M.tensor((2,0), name='t') + sage: e = M.basis('e') + sage: t[0,1] = 7 + sage: t.__ne__(0) + True + sage: t[0,1] = 0 + sage: t.__ne__(0) + False + sage: a = M.tensor((2,0), name='a') # same tensor type as t + sage: a[0,1] = 7 + sage: t.__ne__(a) + True + sage: t[0,1] = 7 + sage: t.__ne__(a) + False + + """ + return not self.__eq__(other) + + def __pos__(self): + r""" + Unary plus operator. + + OUTPUT: + + - an exact copy of ``self`` + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: t = M.tensor((2,0), name='t') + sage: e = M.basis('e') + sage: t[0,1] = 7 + sage: p = t.__pos__() ; p + Type-(2,0) tensor +t on the Rank-3 free module M over the Integer Ring + sage: p.display() + +t = 7 e_0*e_1 + sage: p == t + True + sage: p is t + False + + """ + result = self._new_instance() + for basis in self._components: + result._components[basis] = + self._components[basis] + if self._name is not None: + result._name = '+' + self._name + if self._latex_name is not None: + result._latex_name = '+' + self._latex_name + return result + + def __neg__(self): + r""" + Unary minus operator. + + OUTPUT: + + - the tensor `-T`, where `T` is ``self`` + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: t = M.tensor((2,0), name='t') + sage: e = M.basis('e') + sage: t[0,1], t[1,2] = 7, -4 + sage: t.display() + t = 7 e_0*e_1 - 4 e_1*e_2 + sage: a = t.__neg__() ; a + Type-(2,0) tensor -t on the Rank-3 free module M over the Integer Ring + sage: a.display() + -t = -7 e_0*e_1 + 4 e_1*e_2 + sage: a == -t + True + + """ + result = self._new_instance() + for basis in self._components: + result._components[basis] = - self._components[basis] + if self._name is not None: + result._name = '-' + self._name + if self._latex_name is not None: + result._latex_name = '-' + self._latex_name + return result + + ######### ModuleElement arithmetic operators ######## + + def _add_(self, other): + r""" + Tensor addition. + + INPUT: + + - ``other`` -- a tensor, of the same type as ``self`` + + OUPUT: + + - the tensor resulting from the addition of ``self`` and ``other`` + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 2, name='M') + sage: e = M.basis('e') + sage: a = M.tensor((2,0), name='a') + sage: a[:] = [[4,0], [-2,5]] + sage: b = M.tensor((2,0), name='b') + sage: b[:] = [[0,1], [2,3]] + sage: s = a._add_(b) ; s + Type-(2,0) tensor a+b on the Rank-2 free module M over the Integer Ring + sage: s[:] + [4 1] + [0 8] + sage: a._add_(-a) == 0 + True + sage: a._add_(a) == 2*a + True + + """ + # No need for consistency check since self and other are guaranted + # to belong to the same tensor module + if other == 0: + return +self + basis = self.common_basis(other) + if basis is None: + raise ValueError("no common basis for the addition") + comp_result = self._components[basis] + other._components[basis] + result = self._fmodule.tensor_from_comp(self._tensor_type, comp_result) + if self._name is not None and other._name is not None: + result._name = self._name + '+' + other._name + if self._latex_name is not None and other._latex_name is not None: + result._latex_name = self._latex_name + '+' + other._latex_name + return result + + def _sub_(self, other): + r""" + Tensor subtraction. + + INPUT: + + - ``other`` -- a tensor, of the same type as ``self`` + + OUPUT: + + - the tensor resulting from the subtraction of ``other`` from ``self`` + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 2, name='M') + sage: e = M.basis('e') + sage: a = M.tensor((2,0), name='a') + sage: a[:] = [[4,0], [-2,5]] + sage: b = M.tensor((2,0), name='b') + sage: b[:] = [[0,1], [2,3]] + sage: s = a._sub_(b) ; s + Type-(2,0) tensor a-b on the Rank-2 free module M over the Integer Ring + sage: s[:] + [ 4 -1] + [-4 2] + sage: b._sub_(a) == -s + True + sage: a._sub_(a) == 0 + True + sage: a._sub_(-a) == 2*a + True + + TESTS: + + Check for when there is not a basis, but the same object:: + + sage: M = FiniteRankFreeModule(QQ, 3, name='M') + sage: t = M.tensor((2,1), name='t') + sage: t == t + True + + """ + # No need for consistency check since self and other are guaranted + # to belong to the same tensor module + if other == 0: + return +self + basis = self.common_basis(other) + if basis is None: + raise ValueError("no common basis for the subtraction") + comp_result = self._components[basis] - other._components[basis] + result = self._fmodule.tensor_from_comp(self._tensor_type, comp_result) + if self._name is not None and other._name is not None: + result._name = self._name + '-' + other._name + if self._latex_name is not None and other._latex_name is not None: + result._latex_name = self._latex_name + '-' + other._latex_name + return result + + def _rmul_(self, other): + r""" + Multiplication on the left by ``other``. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 2, name='M') + sage: e = M.basis('e') + sage: a = M.tensor((2,0), name='a') + sage: a[:] = [[4,0], [-2,5]] + sage: s = a._rmul_(2) ; s + Type-(2,0) tensor on the Rank-2 free module M over the Integer Ring + sage: s[:] + [ 8 0] + [-4 10] + sage: s == a + a + True + sage: a._rmul_(0) + Type-(2,0) tensor on the Rank-2 free module M over the Integer Ring + sage: a._rmul_(0) == 0 + True + sage: a._rmul_(1) == a + True + sage: a._rmul_(-1) == -a + True + + """ + #!# The following test is probably not necessary: + if isinstance(other, FreeModuleTensor): + raise NotImplementedError("left tensor product not implemented") + # Left multiplication by a scalar: + result = self._new_instance() + for basis in self._components: + result._components[basis] = other * self._components[basis] + return result + + ######### End of ModuleElement arithmetic operators ######## + + def __mul__(self, other): + r""" + Tensor product. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(ZZ, 2, name='M') + sage: e = M.basis('e') + sage: a = M.tensor((2,0), name='a') + sage: a[:] = [[4,0], [-2,5]] + sage: b = M.tensor((0,2), name='b', antisym=(0,1)) + sage: b[0,1] = 3 + sage: s = a.__mul__(b) ; s + Type-(2,2) tensor a*b on the Rank-2 free module M over the Integer Ring + sage: s.symmetries() + no symmetry; antisymmetry: (2, 3) + sage: s[:] + [[[[0, 12], [-12, 0]], [[0, 0], [0, 0]]], + [[[0, -6], [6, 0]], [[0, 15], [-15, 0]]]] + + """ + from format_utilities import format_mul_txt, format_mul_latex + if isinstance(other, FreeModuleTensor): + basis = self.common_basis(other) + if basis is None: + raise ValueError("no common basis for the tensor product") + comp_prov = self._components[basis] * other._components[basis] + # Reordering of the contravariant and covariant indices: + k1, l1 = self._tensor_type + k2, l2 = other._tensor_type + if l1 != 0: + comp_result = comp_prov.swap_adjacent_indices(k1, + self._tensor_rank, + self._tensor_rank+k2) + else: + comp_result = comp_prov # no reordering is necessary + result = self._fmodule.tensor_from_comp((k1+k2, l1+l2), + comp_result) + result._name = format_mul_txt(self._name, '*', other._name) + result._latex_name = format_mul_latex(self._latex_name, + r'\otimes ', other._latex_name) + return result + + # multiplication by a scalar: + result = self._new_instance() + for basis in self._components: + result._components[basis] = other * self._components[basis] + return result + + + def __div__(self, other): + r""" + Division (by a scalar). + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(QQ, 2, name='M') + sage: e = M.basis('e') + sage: a = M.tensor((2,0), name='a') + sage: a[:] = [[4,0], [-2,5]] + sage: s = a.__div__(4) ; s + Type-(2,0) tensor on the 2-dimensional vector space M over the + Rational Field + sage: s[:] + [ 1 0] + [-1/2 5/4] + sage: 4*s == a + True + sage: s == a/4 + True + + """ + result = self._new_instance() + for basis in self._components: + result._components[basis] = self._components[basis] / other + return result + + + def __call__(self, *args): + r""" + The tensor acting on linear forms and module elements as a multilinear + map. + + INPUT: + + - ``*args`` -- list of `k` linear forms and `l` module elements + with ``self`` being a tensor of type `(k, l)` + + EXAMPLES: + + Action of a type-(2,1) tensor:: + + sage: M = FiniteRankFreeModule(ZZ, 2, name='M') + sage: e = M.basis('e') + sage: t = M.tensor((2,1), name='t', antisym=(0,1)) + sage: t[0,1,0], t[0,1,1] = 3, 2 + sage: t.display() + t = 3 e_0*e_1*e^0 + 2 e_0*e_1*e^1 - 3 e_1*e_0*e^0 - 2 e_1*e_0*e^1 + sage: a = M.linear_form() + sage: a[:] = 1, 2 + sage: b = M.linear_form() + sage: b[:] = 3, -1 + sage: v = M([-2,1]) + sage: t.__call__(a,b,v) + 28 + sage: t(a,b,v) == t.__call__(a,b,v) + True + sage: t(a,b,v) == t.contract(v).contract(b).contract(a) + True + + Action of a linear form on a vector:: + + sage: a.__call__(v) + 0 + sage: a.__call__(v) == a(v) + True + sage: a(v) == a.contract(v) + True + sage: b.__call__(v) + -7 + sage: b.__call__(v) == b(v) + True + sage: b(v) == b.contract(v) + True + + Action of a vector on a linear form:: + + sage: v.__call__(a) + 0 + sage: v.__call__(b) + -7 + + """ + # Consistency checks: + p = len(args) + if p != self._tensor_rank: + raise TypeError(str(self._tensor_rank) + + " arguments must be provided") + for i in range(self._tensor_type[0]): + if not isinstance(args[i], FreeModuleTensor): + raise TypeError("the argument no. " + str(i+1) + + " must be a linear form") + if args[i]._tensor_type != (0,1): + raise TypeError("the argument no. " + str(i+1) + + " must be a linear form") + for i in range(self._tensor_type[0],p): + if not isinstance(args[i], FiniteRankFreeModuleElement): + raise TypeError("the argument no. " + str(i+1) + + " must be a module element") + fmodule = self._fmodule + # + # Specific case of a linear form acting on a vector (for efficiency): + # + if self._tensor_type == (0,1): + vector = args[0] + basis = self.common_basis(vector) + if basis is None: + raise ValueError("no common basis for the components") + omega = self._components[basis] + vv = vector._components[basis] + resu = 0 + for i in fmodule.irange(): + resu += omega[[i]]*vv[[i]] + # Name and LaTeX symbol of the output: + if hasattr(resu, '_name'): + if self._name is not None and vector._name is not None: + resu._name = self._name + "(" + vector._name + ")" + if hasattr(resu, '_latex_name'): + if self._latex_name is not None and \ + vector._latex_name is not None: + resu._latex_name = self._latex_name + r"\left(" + \ + vector._latex_name + r"\right)" + return resu + # + # Generic case + # + # Search for a common basis + basis = None + # First try with the module's default basis + def_basis = fmodule._def_basis + if def_basis in self._components: + basis = def_basis + for arg in args: + if def_basis not in arg._components: + basis = None + break + if basis is None: + # Search for another basis: + for bas in self._components: + basis = bas + for arg in args: + if bas not in arg._components: + basis = None + break + if basis is not None: # common basis found ! + break + if basis is None: + # A last attempt to find a common basis, possibly via a + # change-of-components transformation + for arg in args: + self.common_basis(arg) # to trigger some change of components + for bas in self._components: + basis = bas + for arg in args: + if bas not in arg._components: + basis = None + break + if basis is not None: # common basis found ! + break + if basis is None: + raise ValueError("no common basis for the components") + t = self._components[basis] + v = [args[i]._components[basis] for i in range(p)] + res = 0 + for ind in t.index_generator(): + prod = t[[ind]] + for i in range(p): + prod *= v[i][[ind[i]]] + res += prod + # Name of the output: + if hasattr(res, '_name'): + res_name = None + if self._name is not None: + res_name = self._name + "(" + for i in range(p-1): + if args[i]._name is not None: + res_name += args[i]._name + "," + else: + res_name = None + break + if res_name is not None: + if args[p-1]._name is not None: + res_name += args[p-1]._name + ")" + else: + res_name = None + res._name = res_name + # LaTeX symbol of the output: + if hasattr(res, '_latex_name'): + res_latex = None + if self._latex_name is not None: + res_latex = self._latex_name + r"\left(" + for i in range(p-1): + if args[i]._latex_name is not None: + res_latex += args[i]._latex_name + "," + else: + res_latex = None + break + if res_latex is not None: + if args[p-1]._latex_name is not None: + res_latex += args[p-1]._latex_name + r"\right)" + else: + res_latex = None + res._latex_name = res_latex + return res + + def trace(self, pos1=0, pos2=1): + r""" + Trace (contraction) on two slots of the tensor. + + INPUT: + + - ``pos1`` -- (default: 0) position of the first index for the + contraction, with the convention ``pos1=0`` for the first slot + + - ``pos2`` -- (default: 1) position of the second index for the + contraction, with the same convention as for ``pos1``; the variance + type of ``pos2`` must be opposite to that of ``pos1`` + + OUTPUT: + + - tensor or scalar resulting from the ``(pos1, pos2)`` contraction + + EXAMPLES: + + Trace of a type-`(1,1)` tensor:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') ; e + Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer Ring + sage: a = M.tensor((1,1), name='a') ; a + Type-(1,1) tensor a on the Rank-3 free module M over the Integer Ring + sage: a[:] = [[1,2,3], [4,5,6], [7,8,9]] + sage: a.trace() + 15 + sage: a.trace(0,1) # equivalent to above (contraction of slot 0 with slot 1) + 15 + sage: a.trace(1,0) # the order of the slots does not matter + 15 + + Instead of the explicit call to the method :meth:`trace`, one + may use the index notation with Einstein convention (summation over + repeated indices); it suffices to pass the indices as a string inside + square brackets:: + + sage: a['^i_i'] + 15 + + The letter 'i' to denote the repeated index can be replaced by any + other letter:: + + sage: a['^s_s'] + 15 + + Moreover, the symbol ``^`` can be omitted:: + + sage: a['i_i'] + 15 + + The contraction on two slots having the same tensor type cannot occur:: + + sage: b = M.tensor((2,0), name='b') ; b + Type-(2,0) tensor b on the Rank-3 free module M over the Integer Ring + sage: b[:] = [[1,2,3], [4,5,6], [7,8,9]] + sage: b.trace(0,1) + Traceback (most recent call last): + ... + IndexError: contraction on two contravariant indices is not allowed + + The contraction either preserves or destroys the symmetries:: + + sage: b = M.alternating_form(2, 'b') ; b + Alternating form b of degree 2 on the Rank-3 free module M + over the Integer Ring + sage: b[0,1], b[0,2], b[1,2] = 3, 2, 1 + sage: t = a*b ; t + Type-(1,3) tensor a*b on the Rank-3 free module M + over the Integer Ring + + By construction, ``t`` is a tensor field antisymmetric w.r.t. its + last two slots:: + + sage: t.symmetries() + no symmetry; antisymmetry: (2, 3) + sage: s = t.trace(0,1) ; s # contraction on the first two slots + Alternating form of degree 2 on the + Rank-3 free module M over the Integer Ring + sage: s.symmetries() # the antisymmetry is preserved + no symmetry; antisymmetry: (0, 1) + sage: s[:] + [ 0 45 30] + [-45 0 15] + [-30 -15 0] + sage: s == 15*b # check + True + sage: s = t.trace(0,2) ; s # contraction on the first and third slots + Type-(0,2) tensor on the Rank-3 free module M over the Integer Ring + sage: s.symmetries() # the antisymmetry has been destroyed by the above contraction: + no symmetry; no antisymmetry + sage: s[:] # indeed: + [-26 -4 6] + [-31 -2 9] + [-36 0 12] + sage: s[:] == matrix( [[sum(t[k,i,k,j] for k in M.irange()) + ....: for j in M.irange()] for i in M.irange()] ) # check + True + + Use of index notation instead of :meth:`trace`:: + + sage: t['^k_kij'] == t.trace(0,1) + True + sage: t['^k_{kij}'] == t.trace(0,1) # LaTeX notation + True + sage: t['^k_ikj'] == t.trace(0,2) + True + sage: t['^k_ijk'] == t.trace(0,3) + True + + Index symbols not involved in the contraction may be replaced by + dots:: + + sage: t['^k_k..'] == t.trace(0,1) + True + sage: t['^k_.k.'] == t.trace(0,2) + True + sage: t['^k_..k'] == t.trace(0,3) + True + + """ + # The indices at pos1 and pos2 must be of different types: + k_con = self._tensor_type[0] + l_cov = self._tensor_type[1] + if pos1 < k_con and pos2 < k_con: + raise IndexError("contraction on two contravariant indices is " + + "not allowed") + if pos1 >= k_con and pos2 >= k_con: + raise IndexError("contraction on two covariant indices is " + + "not allowed") + # Frame selection for the computation: + if self._fmodule._def_basis in self._components: + basis = self._fmodule._def_basis + else: # a basis is picked arbitrarily: + basis = self.pick_a_basis() + resu_comp = self._components[basis].trace(pos1, pos2) + if self._tensor_rank == 2: # result is a scalar + return resu_comp + else: + return self._fmodule.tensor_from_comp((k_con-1, l_cov-1), + resu_comp) + + def contract(self, *args): + r""" + Contraction on one or more indices with another tensor. + + INPUT: + + - ``pos1`` -- positions of the indices in ``self`` involved in the + contraction; ``pos1`` must be a sequence of integers, with 0 standing + for the first index position, 1 for the second one, etc; if ``pos1`` + is not provided, a single contraction on the last index position of + ``self`` is assumed + - ``other`` -- the tensor to contract with + - ``pos2`` -- positions of the indices in ``other`` involved in the + contraction, with the same conventions as for ``pos1``; if ``pos2`` + is not provided, a single contraction on the first index position of + ``other`` is assumed + + OUTPUT: + + - tensor resulting from the contraction at the positions ``pos1`` and + ``pos2`` of ``self`` with ``other`` + + EXAMPLES: + + Contraction of a tensor of type `(0,1)` with a tensor of type `(1,0)`:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: a = M.linear_form() # tensor of type (0,1) is a linear form + sage: a[:] = [-3,2,1] + sage: b = M([2,5,-2]) # tensor of type (1,0) is a module element + sage: s = a.contract(b) ; s + 2 + sage: s in M.base_ring() + True + sage: s == a[0]*b[0] + a[1]*b[1] + a[2]*b[2] # check of the computation + True + + The positions of the contraction indices can be set explicitely:: + + sage: s == a.contract(0, b, 0) + True + sage: s == a.contract(0, b) + True + sage: s == a.contract(b, 0) + True + + Instead of the explicit call to the method :meth:`contract`, the index + notation can be used to specify the contraction, via Einstein + conventation (summation on repeated indices); it suffices to pass the + indices as a string inside square brackets:: + + sage: s1 = a['_i']*b['^i'] ; s1 + 2 + sage: s1 == s + True + + In the present case, performing the contraction is identical to + applying the linear form to the module element:: + + sage: a.contract(b) == a(b) + True + + or to applying the module element, considered as a tensor of type (1,0), + to the linear form:: + + sage: a.contract(b) == b(a) + True + + We have also:: + + sage: a.contract(b) == b.contract(a) + True + + Contraction of a tensor of type `(1,1)` with a tensor of type `(1,0)`:: + + sage: a = M.tensor((1,1)) + sage: a[:] = [[-1,2,3],[4,-5,6],[7,8,9]] + sage: s = a.contract(b) ; s + Element of the Rank-3 free module M over the Integer Ring + sage: s.display() + 2 e_0 - 29 e_1 + 36 e_2 + + Since the index positions have not been specified, the contraction + takes place on the last position of a (i.e. no. 1) and the first + position of ``b`` (i.e. no. 0):: + + sage: a.contract(b) == a.contract(1, b, 0) + True + sage: a.contract(b) == b.contract(0, a, 1) + True + sage: a.contract(b) == b.contract(a, 1) + True + + Using the index notation with Einstein convention:: + + sage: a['^i_j']*b['^j'] == a.contract(b) + True + + The index ``i`` can be replaced by a dot:: + + sage: a['^._j']*b['^j'] == a.contract(b) + True + + and the symbol ``^`` may be omitted, the distinction between + contravariant and covariant indices being the position with respect to + the symbol ``_``:: + + sage: a['._j']*b['j'] == a.contract(b) + True + + Contraction is possible only between a contravariant index and a + covariant one:: + + sage: a.contract(0, b) + Traceback (most recent call last): + ... + TypeError: contraction on two contravariant indices not permitted + + Contraction of a tensor of type `(2,1)` with a tensor of type `(0,2)`:: + + sage: a = a*b ; a + Type-(2,1) tensor on the Rank-3 free module M over the Integer Ring + sage: b = M.tensor((0,2)) + sage: b[:] = [[-2,3,1], [0,-2,3], [4,-7,6]] + sage: s = a.contract(1, b, 1) ; s + Type-(1,2) tensor on the Rank-3 free module M over the Integer Ring + sage: s[:] + [[[-9, 16, 39], [18, -32, -78], [27, -48, -117]], + [[36, -64, -156], [-45, 80, 195], [54, -96, -234]], + [[63, -112, -273], [72, -128, -312], [81, -144, -351]]] + + Check of the computation:: + + sage: all(s[i,j,k] == a[i,0,j]*b[k,0]+a[i,1,j]*b[k,1]+a[i,2,j]*b[k,2] + ....: for i in range(3) for j in range(3) for k in range(3)) + True + + Using index notation:: + + sage: a['il_j']*b['_kl'] == a.contract(1, b, 1) + True + + LaTeX notation are allowed:: + + sage: a['^{il}_j']*b['_{kl}'] == a.contract(1, b, 1) + True + + Indices not involved in the contraction may be replaced by dots:: + + sage: a['.l_.']*b['_.l'] == a.contract(1, b, 1) + True + + The two tensors do not have to be defined on the same basis for the + contraction to take place, reflecting the fact that the contraction is + basis-independent:: + + sage: A = M.automorphism() + sage: A[:] = [[0,0,1], [1,0,0], [0,-1,0]] + sage: h = e.new_basis(A, 'h') + sage: b.comp(h)[:] # forces the computation of b's components w.r.t. basis h + [-2 -3 0] + [ 7 6 -4] + [ 3 -1 -2] + sage: b.del_other_comp(h) # deletes components w.r.t. basis e + sage: b._components.keys() # indeed: + [Basis (h_0,h_1,h_2) on the Rank-3 free module M over the Integer Ring] + sage: a._components.keys() # while a is known only in basis e: + [Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer Ring] + sage: s1 = a.contract(1, b, 1) ; s1 # yet the computation is possible + Type-(1,2) tensor on the Rank-3 free module M over the Integer Ring + sage: s1 == s # ... and yields the same result as previously: + True + + The contraction can be performed on more than a single index; for + instance a `2`-indices contraction of a type-`(2,1)` tensor with a + type-`(1,2)` one is:: + + sage: a # a is a tensor of type-(2,1) + Type-(2,1) tensor on the Rank-3 free module M over the Integer Ring + sage: b = M([1,-1,2])*b ; b # a tensor of type (1,2) + Type-(1,2) tensor on the Rank-3 free module M over the Integer Ring + sage: s = a.contract(1,2,b,1,0) ; s # the double contraction + Type-(1,1) tensor on the Rank-3 free module M over the Integer Ring + sage: s[:] + [ -36 30 15] + [-252 210 105] + [-204 170 85] + sage: s == a['^.k_l']*b['^l_k.'] # the same thing in index notation + True + + """ + # + # Treatment of the input + # + nargs = len(args) + for i, arg in enumerate(args): + if isinstance(arg, FreeModuleTensor): + other = arg + it = i + break + else: + raise TypeError("a tensor must be provided in the argument list") + if it == 0: + pos1 = (self._tensor_rank - 1,) + else: + pos1 = args[:it] + if it == nargs-1: + pos2 = (0,) + else: + pos2 = args[it+1:] + ncontr = len(pos1) # number of contractions + if len(pos2) != ncontr: + raise TypeError("different number of indices for the contraction") + k1, l1 = self._tensor_type + k2, l2 = other._tensor_type + for i in range(ncontr): + p1 = pos1[i] + p2 = pos2[i] + if p1 < k1 and p2 < k2: + raise TypeError("contraction on two contravariant indices " + + "not permitted") + if p1 >= k1 and p2 >= k2: + raise TypeError("contraction on two covariant indices " + + "not permitted") + # + # Contraction at the component level + # + basis = self.common_basis(other) + if basis is None: + raise ValueError("no common basis for the contraction") + args = pos1 + (other._components[basis],) + pos2 + cmp_res = self._components[basis].contract(*args) + if self._tensor_rank + other._tensor_rank - 2*ncontr == 0: + # Case of scalar output: + return cmp_res + # + # Reordering of the indices to have all contravariant indices first: + # + nb_cov_s = 0 # Number of covariant indices of self not involved in the + # contraction + for pos in range(k1,k1+l1): + if pos not in pos1: + nb_cov_s += 1 + nb_con_o = 0 # Number of contravariant indices of other not involved + # in the contraction + for pos in range(0,k2): + if pos not in pos2: + nb_con_o += 1 + if nb_cov_s != 0 and nb_con_o !=0: + # some reodering is necessary: + p2 = k1 + l1 - ncontr + p1 = p2 - nb_cov_s + p3 = p2 + nb_con_o + cmp_res = cmp_res.swap_adjacent_indices(p1, p2, p3) + type_res = (k1+k2-ncontr, l1+l2-ncontr) + return self._fmodule.tensor_from_comp(type_res, cmp_res) + + def symmetrize(self, *pos, **kwargs): + r""" + Symmetrization over some arguments. + + INPUT: + + - ``pos`` -- list of argument positions involved in the + symmetrization (with the convention ``position=0`` for the first + argument); if none, the symmetrization is performed over all the + arguments + - ``basis`` -- (default: ``None``) module basis with respect to which + the component computation is to be performed; if none, the module's + default basis is used if the tensor field has already components + in it; otherwise another basis w.r.t. which the tensor has + components will be picked + + OUTPUT: + + - the symmetrized tensor (instance of :class:`FreeModuleTensor`) + + EXAMPLES: + + Symmetrization of a tensor of type `(2,0)`:: + + sage: M = FiniteRankFreeModule(QQ, 3, name='M') + sage: e = M.basis('e') + sage: t = M.tensor((2,0)) + sage: t[:] = [[2,1,-3],[0,-4,5],[-1,4,2]] + sage: s = t.symmetrize() ; s + Type-(2,0) tensor on the 3-dimensional vector space M over the + Rational Field + sage: t[:], s[:] + ( + [ 2 1 -3] [ 2 1/2 -2] + [ 0 -4 5] [1/2 -4 9/2] + [-1 4 2], [ -2 9/2 2] + ) + sage: s.symmetries() + symmetry: (0, 1); no antisymmetry + sage: all(s[i,j] == 1/2*(t[i,j]+t[j,i]) # check: + ....: for i in range(3) for j in range(3)) + True + + Instead of invoking the method :meth:`symmetrize`, one may use the + index notation with parentheses to denote the symmetrization; it + suffices to pass the indices as a string inside square brackets:: + + sage: t['(ij)'] + Type-(2,0) tensor on the 3-dimensional vector space M over the + Rational Field + sage: t['(ij)'].symmetries() + symmetry: (0, 1); no antisymmetry + sage: t['(ij)'] == t.symmetrize() + True + + The indices names are not significant; they can even be replaced by + dots:: + + sage: t['(..)'] == t.symmetrize() + True + + The LaTeX notation can be used as well:: + + sage: t['^{(ij)}'] == t.symmetrize() + True + + Symmetrization of a tensor of type `(0,3)` on the first two arguments:: + + sage: t = M.tensor((0,3)) + sage: t[:] = [[[1,2,3], [-4,5,6], [7,8,-9]], + ....: [[10,-11,12], [13,14,-15], [16,17,18]], + ....: [[19,-20,-21], [-22,23,24], [25,26,-27]]] + sage: s = t.symmetrize(0,1) ; s # (0,1) = the first two arguments + Type-(0,3) tensor on the 3-dimensional vector space M over the + Rational Field + sage: s.symmetries() + symmetry: (0, 1); no antisymmetry + sage: s[:] + [[[1, 2, 3], [3, -3, 9], [13, -6, -15]], + [[3, -3, 9], [13, 14, -15], [-3, 20, 21]], + [[13, -6, -15], [-3, 20, 21], [25, 26, -27]]] + sage: all(s[i,j,k] == 1/2*(t[i,j,k]+t[j,i,k]) # Check: + ....: for i in range(3) for j in range(3) for k in range(3)) + True + sage: s.symmetrize(0,1) == s # another test + True + + Again the index notation can be used:: + + sage: t['_(ij)k'] == t.symmetrize(0,1) + True + sage: t['_(..).'] == t.symmetrize(0,1) # no index name + True + sage: t['_{(ij)k}'] == t.symmetrize(0,1) # LaTeX notation + True + sage: t['_{(..).}'] == t.symmetrize(0,1) # this also allowed + True + + Symmetrization of a tensor of type `(0,3)` on the first and + last arguments:: + + sage: s = t.symmetrize(0,2) ; s # (0,2) = first and last arguments + Type-(0,3) tensor on the 3-dimensional vector space M over the + Rational Field + sage: s.symmetries() + symmetry: (0, 2); no antisymmetry + sage: s[:] + [[[1, 6, 11], [-4, 9, -8], [7, 12, 8]], + [[6, -11, -4], [9, 14, 4], [12, 17, 22]], + [[11, -4, -21], [-8, 4, 24], [8, 22, -27]]] + sage: all(s[i,j,k] == 1/2*(t[i,j,k]+t[k,j,i]) + ....: for i in range(3) for j in range(3) for k in range(3)) + True + sage: s.symmetrize(0,2) == s # another test + True + + Symmetrization of a tensor of type `(0,3)` on the last two arguments:: + + sage: s = t.symmetrize(1,2) ; s # (1,2) = the last two arguments + Type-(0,3) tensor on the 3-dimensional vector space M over the + Rational Field + sage: s.symmetries() + symmetry: (1, 2); no antisymmetry + sage: s[:] + [[[1, -1, 5], [-1, 5, 7], [5, 7, -9]], + [[10, 1, 14], [1, 14, 1], [14, 1, 18]], + [[19, -21, 2], [-21, 23, 25], [2, 25, -27]]] + sage: all(s[i,j,k] == 1/2*(t[i,j,k]+t[i,k,j]) # Check: + ....: for i in range(3) for j in range(3) for k in range(3)) + True + sage: s.symmetrize(1,2) == s # another test + True + + Use of the index notation:: + + sage: t['_i(jk)'] == t.symmetrize(1,2) + True + sage: t['_.(..)'] == t.symmetrize(1,2) + True + sage: t['_{i(jk)}'] == t.symmetrize(1,2) # LaTeX notation + True + + Full symmetrization of a tensor of type `(0,3)`:: + + sage: s = t.symmetrize() ; s + Type-(0,3) tensor on the 3-dimensional vector space M over the + Rational Field + sage: s.symmetries() + symmetry: (0, 1, 2); no antisymmetry + sage: s[:] + [[[1, 8/3, 29/3], [8/3, 7/3, 0], [29/3, 0, -5/3]], + [[8/3, 7/3, 0], [7/3, 14, 25/3], [0, 25/3, 68/3]], + [[29/3, 0, -5/3], [0, 25/3, 68/3], [-5/3, 68/3, -27]]] + sage: all(s[i,j,k] == 1/6*(t[i,j,k]+t[i,k,j]+t[j,k,i]+t[j,i,k]+t[k,i,j]+t[k,j,i]) # Check: + ....: for i in range(3) for j in range(3) for k in range(3)) + True + sage: s.symmetrize() == s # another test + True + + Index notation for the full symmetrization:: + + sage: t['_(ijk)'] == t.symmetrize() + True + sage: t['_{(ijk)}'] == t.symmetrize() # LaTeX notation + True + + Symmetrization can be performed only on arguments on the same type:: + + sage: t = M.tensor((1,2)) + sage: t[:] = [[[1,2,3], [-4,5,6], [7,8,-9]], + ....: [[10,-11,12], [13,14,-15], [16,17,18]], + ....: [[19,-20,-21], [-22,23,24], [25,26,-27]]] + sage: s = t.symmetrize(0,1) + Traceback (most recent call last): + ... + TypeError: 0 is a contravariant position, while 1 is a covariant position; + symmetrization is meaningfull only on tensor arguments of the same type + sage: s = t.symmetrize(1,2) # OK: both 1 and 2 are covariant positions + + The order of positions does not matter:: + + sage: t.symmetrize(2,1) == t.symmetrize(1,2) + True + + Use of the index notation:: + + sage: t['^i_(jk)'] == t.symmetrize(1,2) + True + sage: t['^._(..)'] == t.symmetrize(1,2) + True + + The character ``^`` can be skipped, the character ``_`` being + sufficient to separate contravariant indices from covariant ones:: + + sage: t['i_(jk)'] == t.symmetrize(1,2) + True + + The LaTeX notation can be employed:: + + sage: t['^{i}_{(jk)}'] == t.symmetrize(1,2) + True + + """ + if not pos: + pos = range(self._tensor_rank) + # check whether the symmetrization is possible: + pos_cov = self._tensor_type[0] # first covariant position + pos0 = pos[0] + if pos0 < pos_cov: # pos0 is a contravariant position + for k in range(1,len(pos)): + if pos[k] >= pos_cov: + raise TypeError( + str(pos[0]) + " is a contravariant position, while " + + str(pos[k]) + " is a covariant position; \n" + "symmetrization is meaningfull only on tensor " + + "arguments of the same type") + else: # pos0 is a covariant position + for k in range(1,len(pos)): + if pos[k] < pos_cov: + raise TypeError( + str(pos[0]) + " is a covariant position, while " + \ + str(pos[k]) + " is a contravariant position; \n" + "symmetrization is meaningfull only on tensor " + + "arguments of the same type") + if 'basis' in kwargs: + basis = kwargs['basis'] + else: + basis = self.pick_a_basis() + res_comp = self._components[basis].symmetrize(*pos) + return self._fmodule.tensor_from_comp(self._tensor_type, res_comp) + + + def antisymmetrize(self, *pos, **kwargs): + r""" + Antisymmetrization over some arguments. + + INPUT: + + - ``pos`` -- list of argument positions involved in the + antisymmetrization (with the convention ``position=0`` for the first + argument); if none, the antisymmetrization is performed over all the + arguments + - ``basis`` -- (default: ``None``) module basis with respect to which + the component computation is to be performed; if none, the module's + default basis is used if the tensor field has already components + in it; otherwise another basis w.r.t. which the tensor has + components will be picked + + OUTPUT: + + - the antisymmetrized tensor (instance of :class:`FreeModuleTensor`) + + EXAMPLES: + + Antisymmetrization of a tensor of type `(2,0)`:: + + sage: M = FiniteRankFreeModule(QQ, 3, name='M') + sage: e = M.basis('e') + sage: t = M.tensor((2,0)) + sage: t[:] = [[1,-2,3], [4,5,6], [7,8,-9]] + sage: s = t.antisymmetrize() ; s + Type-(2,0) tensor on the 3-dimensional vector space M over the + Rational Field + sage: s.symmetries() + no symmetry; antisymmetry: (0, 1) + sage: t[:], s[:] + ( + [ 1 -2 3] [ 0 -3 -2] + [ 4 5 6] [ 3 0 -1] + [ 7 8 -9], [ 2 1 0] + ) + sage: all(s[i,j] == 1/2*(t[i,j]-t[j,i]) # Check: + ....: for i in range(3) for j in range(3)) + True + sage: s.antisymmetrize() == s # another test + True + sage: t.antisymmetrize() == t.antisymmetrize(0,1) + True + + Antisymmetrization of a tensor of type `(0, 3)` over the first two + arguments:: + + sage: t = M.tensor((0,3)) + sage: t[:] = [[[1,2,3], [-4,5,6], [7,8,-9]], + ....: [[10,-11,12], [13,14,-15], [16,17,18]], + ....: [[19,-20,-21], [-22,23,24], [25,26,-27]]] + sage: s = t.antisymmetrize(0,1) ; s # (0,1) = the first two arguments + Type-(0,3) tensor on the 3-dimensional vector space M over the + Rational Field + sage: s.symmetries() + no symmetry; antisymmetry: (0, 1) + sage: s[:] + [[[0, 0, 0], [-7, 8, -3], [-6, 14, 6]], + [[7, -8, 3], [0, 0, 0], [19, -3, -3]], + [[6, -14, -6], [-19, 3, 3], [0, 0, 0]]] + sage: all(s[i,j,k] == 1/2*(t[i,j,k]-t[j,i,k]) # Check: + ....: for i in range(3) for j in range(3) for k in range(3)) + True + sage: s.antisymmetrize(0,1) == s # another test + True + sage: s.symmetrize(0,1) == 0 # of course + True + + Instead of invoking the method :meth:`antisymmetrize`, one can use + the index notation with square brackets denoting the + antisymmetrization; it suffices to pass the indices as a string + inside square brackets:: + + sage: s1 = t['_[ij]k'] ; s1 + Type-(0,3) tensor on the 3-dimensional vector space M over the + Rational Field + sage: s1.symmetries() + no symmetry; antisymmetry: (0, 1) + sage: s1 == s + True + + The LaTeX notation is recognized:: + + sage: t['_{[ij]k}'] == s + True + + Note that in the index notation, the name of the indices is irrelevant; + they can even be replaced by dots:: + + sage: t['_[..].'] == s + True + + Antisymmetrization of a tensor of type (0,3) over the first and last + arguments:: + + sage: s = t.antisymmetrize(0,2) ; s # (0,2) = first and last arguments + Type-(0,3) tensor on the 3-dimensional vector space M over the + Rational Field + sage: s.symmetries() + no symmetry; antisymmetry: (0, 2) + sage: s[:] + [[[0, -4, -8], [0, -4, 14], [0, -4, -17]], + [[4, 0, 16], [4, 0, -19], [4, 0, -4]], + [[8, -16, 0], [-14, 19, 0], [17, 4, 0]]] + sage: all(s[i,j,k] == 1/2*(t[i,j,k]-t[k,j,i]) # Check: + ....: for i in range(3) for j in range(3) for k in range(3)) + True + sage: s.antisymmetrize(0,2) == s # another test + True + sage: s.symmetrize(0,2) == 0 # of course + True + sage: s.symmetrize(0,1) == 0 # no reason for this to hold + False + + Antisymmetrization of a tensor of type `(0,3)` over the last two + arguments:: + + sage: s = t.antisymmetrize(1,2) ; s # (1,2) = the last two arguments + Type-(0,3) tensor on the 3-dimensional vector space M over the + Rational Field + sage: s.symmetries() + no symmetry; antisymmetry: (1, 2) + sage: s[:] + [[[0, 3, -2], [-3, 0, -1], [2, 1, 0]], + [[0, -12, -2], [12, 0, -16], [2, 16, 0]], + [[0, 1, -23], [-1, 0, -1], [23, 1, 0]]] + sage: all(s[i,j,k] == 1/2*(t[i,j,k]-t[i,k,j]) # Check: + ....: for i in range(3) for j in range(3) for k in range(3)) + True + sage: s.antisymmetrize(1,2) == s # another test + True + sage: s.symmetrize(1,2) == 0 # of course + True + + The index notation can be used instead of the explicit call to + :meth:`antisymmetrize`:: + + sage: t['_i[jk]'] == t.antisymmetrize(1,2) + True + + Full antisymmetrization of a tensor of type (0,3):: + + sage: s = t.antisymmetrize() ; s + Alternating form of degree 3 on the 3-dimensional vector space M + over the Rational Field + sage: s.symmetries() + no symmetry; antisymmetry: (0, 1, 2) + sage: s[:] + [[[0, 0, 0], [0, 0, 2/3], [0, -2/3, 0]], + [[0, 0, -2/3], [0, 0, 0], [2/3, 0, 0]], + [[0, 2/3, 0], [-2/3, 0, 0], [0, 0, 0]]] + sage: all(s[i,j,k] == 1/6*(t[i,j,k]-t[i,k,j]+t[j,k,i]-t[j,i,k] + ....: +t[k,i,j]-t[k,j,i]) + ....: for i in range(3) for j in range(3) for k in range(3)) + True + sage: s.antisymmetrize() == s # another test + True + sage: s.symmetrize(0,1) == 0 # of course + True + sage: s.symmetrize(0,2) == 0 # of course + True + sage: s.symmetrize(1,2) == 0 # of course + True + sage: t.antisymmetrize() == t.antisymmetrize(0,1,2) + True + + The index notation can be used instead of the explicit call to + :meth:`antisymmetrize`:: + + sage: t['_[ijk]'] == t.antisymmetrize() + True + sage: t['_[abc]'] == t.antisymmetrize() + True + sage: t['_[...]'] == t.antisymmetrize() + True + sage: t['_{[ijk]}'] == t.antisymmetrize() # LaTeX notation + True + + Antisymmetrization can be performed only on arguments on the same type:: + + sage: t = M.tensor((1,2)) + sage: t[:] = [[[1,2,3], [-4,5,6], [7,8,-9]], + ....: [[10,-11,12], [13,14,-15], [16,17,18]], + ....: [[19,-20,-21], [-22,23,24], [25,26,-27]]] + sage: s = t.antisymmetrize(0,1) + Traceback (most recent call last): + ... + TypeError: 0 is a contravariant position, while 1 is a covariant position; + antisymmetrization is meaningfull only on tensor arguments of the same type + sage: s = t.antisymmetrize(1,2) # OK: both 1 and 2 are covariant positions + + The order of positions does not matter:: + + sage: t.antisymmetrize(2,1) == t.antisymmetrize(1,2) + True + + Again, the index notation can be used:: + + sage: t['^i_[jk]'] == t.antisymmetrize(1,2) + True + sage: t['^i_{[jk]}'] == t.antisymmetrize(1,2) # LaTeX notation + True + + The character '^' can be skipped:: + + sage: t['i_[jk]'] == t.antisymmetrize(1,2) + True + + """ + if not pos: + pos = range(self._tensor_rank) + # check whether the antisymmetrization is possible: + pos_cov = self._tensor_type[0] # first covariant position + pos0 = pos[0] + if pos0 < pos_cov: # pos0 is a contravariant position + for k in range(1,len(pos)): + if pos[k] >= pos_cov: + raise TypeError( + str(pos[0]) + " is a contravariant position, while " + + str(pos[k]) + " is a covariant position; \n" + "antisymmetrization is meaningfull only on tensor " + + "arguments of the same type") + else: # pos0 is a covariant position + for k in range(1,len(pos)): + if pos[k] < pos_cov: + raise TypeError( + str(pos[0]) + " is a covariant position, while " + \ + str(pos[k]) + " is a contravariant position; \n" + "antisymmetrization is meaningfull only on tensor " + + "arguments of the same type") + if 'basis' in kwargs: + basis = kwargs['basis'] + else: + basis = self.pick_a_basis() + res_comp = self._components[basis].antisymmetrize(*pos) + return self._fmodule.tensor_from_comp(self._tensor_type, res_comp) + + +#****************************************************************************** + +# From sage/modules/module.pyx: +#----------------------------- +### The Element should also implement _rmul_ (or _lmul_) +# +# class MyElement(sage.structure.element.ModuleElement): +# def _rmul_(self, c): +# ... + + +class FiniteRankFreeModuleElement(FreeModuleTensor): + r""" + Element of a free module of finite rank over a commutative ring. + + This is a Sage *element* class, the corresponding *parent* class being + :class:`~sage.tensor.modules.finite_rank_free_module.FiniteRankFreeModule`. + + The class :class:`FiniteRankFreeModuleElement` inherits from + :class:`FreeModuleTensor` because the elements of a free module `M` of + finite rank over a commutative ring `R` are identified with tensors of + type `(1,0)` on `M` via the canonical map + + .. MATH:: + + \begin{array}{lllllll} + \Phi: & M & \longrightarrow & M^{**} & & & \\ + & v & \longmapsto & \bar v : & M^* & \longrightarrow & R \\ + & & & & a & \longmapsto & a(v) + \end{array} + + Note that for free modules of finite rank, this map is actually an + isomorphism, enabling the canonical identification: `M^{**}= M`. + + INPUT: + + - ``fmodule`` -- free module `M` of finite rank over a commutative ring + `R`, as an instance of + :class:`~sage.tensor.modules.finite_rank_free_module.FiniteRankFreeModule` + - ``name`` -- (default: ``None``) name given to the element + - ``latex_name`` -- (default: ``None``) LaTeX symbol to denote the element; + if none is provided, the LaTeX symbol is set to ``name`` + + EXAMPLES: + + Let us consider a rank-3 free module `M` over `\ZZ`:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') ; e + Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer Ring + + There are three ways to construct an element of the free module `M`: + the first one (recommended) is using the free module:: + + sage: v = M([2,0,-1], basis=e, name='v') ; v + Element v of the Rank-3 free module M over the Integer Ring + sage: v.display() # expansion on the default basis (e) + v = 2 e_0 - e_2 + sage: v.parent() is M + True + + The second way is to construct a tensor of type `(1,0)` on `M` (cf. the + canonical identification `M^{**} = M` recalled above):: + + sage: v2 = M.tensor((1,0), name='v') + sage: v2[0], v2[2] = 2, -1 ; v2 + Element v of the Rank-3 free module M over the Integer Ring + sage: v2.display() + v = 2 e_0 - e_2 + sage: v2 == v + True + + Finally, the third way is via some linear combination of the basis + elements:: + + sage: v3 = 2*e[0] - e[2] + sage: v3.set_name('v') ; v3 # in this case, the name has to be set separately + Element v of the Rank-3 free module M over the Integer Ring + sage: v3.display() + v = 2 e_0 - e_2 + sage: v3 == v + True + + The canonical identification `M^{**} = M` is implemented by letting the + module elements act on linear forms, providing the same result as the + reverse operation (cf. the map `\Phi` defined above):: + + sage: a = M.linear_form(name='a') + sage: a[:] = (2, 1, -3) ; a + Linear form a on the Rank-3 free module M over the Integer Ring + sage: v(a) + 7 + sage: a(v) + 7 + sage: a(v) == v(a) + True + + .. RUBRIC:: ARITHMETIC EXAMPLES + + Addition:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') ; e + Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer Ring + sage: a = M([0,1,3], name='a') ; a + Element a of the Rank-3 free module M over the Integer Ring + sage: a.display() + a = e_1 + 3 e_2 + sage: b = M([2,-2,1], name='b') ; b + Element b of the Rank-3 free module M over the Integer Ring + sage: b.display() + b = 2 e_0 - 2 e_1 + e_2 + sage: s = a + b ; s + Element a+b of the Rank-3 free module M over the Integer Ring + sage: s.display() + a+b = 2 e_0 - e_1 + 4 e_2 + sage: all(s[i] == a[i] + b[i] for i in M.irange()) + True + + Subtraction:: + + sage: s = a - b ; s + Element a-b of the Rank-3 free module M over the Integer Ring + sage: s.display() + a-b = -2 e_0 + 3 e_1 + 2 e_2 + sage: all(s[i] == a[i] - b[i] for i in M.irange()) + True + + Multiplication by a scalar:: + + sage: s = 2*a ; s + Element of the Rank-3 free module M over the Integer Ring + sage: s.display() + 2 e_1 + 6 e_2 + sage: a.display() + a = e_1 + 3 e_2 + + Tensor product:: + + sage: s = a*b ; s + Type-(2,0) tensor a*b on the Rank-3 free module M over the Integer Ring + sage: s.symmetries() + no symmetry; no antisymmetry + sage: s[:] + [ 0 0 0] + [ 2 -2 1] + [ 6 -6 3] + sage: s = a*s ; s + Type-(3,0) tensor a*a*b on the Rank-3 free module M over the Integer Ring + sage: s[:] + [[[0, 0, 0], [0, 0, 0], [0, 0, 0]], + [[0, 0, 0], [2, -2, 1], [6, -6, 3]], + [[0, 0, 0], [6, -6, 3], [18, -18, 9]]] + + """ + def __init__(self, fmodule, name=None, latex_name=None): + r""" + TESTS:: + + sage: from sage.tensor.modules.free_module_tensor import FiniteRankFreeModuleElement + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: v = FiniteRankFreeModuleElement(M, name='v') + sage: v[e,:] = (-2, 1, 3) + sage: TestSuite(v).run(skip="_test_category") # see below + + In the above test suite, _test_category fails because v is not an + instance of v.parent().category().element_class. Actually module + elements must be constructed via FiniteRankFreeModule.element_class and + not by a direct call to FiniteRankFreeModuleElement:: + + sage: v1 = M.element_class(M, name='v') + sage: v1[e,:] = (-2, 1, 3) + sage: TestSuite(v1).run() + + """ + FreeModuleTensor.__init__(self, fmodule, (1,0), name=name, + latex_name=latex_name) + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLE:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: M([1,-2,3], name='v') + Element v of the Rank-3 free module M over the Integer Ring + + """ + description = "Element " + if self._name is not None: + description += self._name + " " + description += "of the {}".format(self._fmodule) + return description + + def _new_comp(self, basis): + r""" + Create some (uninitialized) components of ``self`` in a given basis. + + This method, which is already implemented in + :meth:`FreeModuleTensor._new_comp`, is redefined here for efficiency. + + INPUT: + + - ``basis`` -- basis of the free module on which ``self`` is defined + + OUTPUT: + + - an instance of :class:`~sage.tensor.modules.comp.Components` + + EXAMPLE:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: v = M([1,-2,3], name='v') + sage: v._new_comp(e) + 1-index components w.r.t. Basis (e_0,e_1,e_2) on the + Rank-3 free module M over the Integer Ring + sage: type(v._new_comp(e)) + + + """ + fmodule = self._fmodule # the base free module + return Components(fmodule._ring, basis, 1, start_index=fmodule._sindex, + output_formatter=fmodule._output_formatter) + + + def _new_instance(self): + r""" + Create an instance of the same class as ``self``. + + EXAMPLE:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: v = M([1,-2,3], name='v') + sage: v._new_instance() + Element of the Rank-3 free module M over the Integer Ring + sage: v._new_instance().parent() is v.parent() + True + + """ + return self.__class__(self._fmodule) diff --git a/src/sage/tensor/modules/tensor_free_module.py b/src/sage/tensor/modules/tensor_free_module.py new file mode 100644 index 00000000000..afd3fd71882 --- /dev/null +++ b/src/sage/tensor/modules/tensor_free_module.py @@ -0,0 +1,642 @@ +r""" +Tensor products of free modules + +The class :class:`TensorFreeModule` implements tensor products of the type + +.. MATH:: + + T^{(k,l)}(M) = \underbrace{M\otimes\cdots\otimes M}_{k\ \; \mbox{times}} + \otimes \underbrace{M^*\otimes\cdots\otimes M^*}_{l\ \; \mbox{times}}, + +where `M` is a free module of finite rank over a commutative ring `R` and +`M^*=\mathrm{Hom}_R(M,R)` is the dual of `M`. +Note that `T^{(1,0)}(M) = M` and `T^{(0,1)}(M) = M^*`. + +Thanks to the canonical isomorphism `M^{**} \simeq M` (which holds since `M` +is a free module of finite rank), `T^{(k,l)}(M)` can be identified with the +set of tensors of type `(k,l)` defined as multilinear maps + +.. MATH:: + + \underbrace{M^*\times\cdots\times M^*}_{k\ \; \mbox{times}} + \times \underbrace{M\times\cdots\times M}_{l\ \; \mbox{times}} + \longrightarrow R + +Accordingly, :class:`TensorFreeModule` is a Sage *parent* class, whose +*element* class is +:class:`~sage.tensor.modules.free_module_tensor.FreeModuleTensor`. + +`T^{(k,l)}(M)` is itself a free module over `R`, of rank `n^{k+l}`, `n` +being the rank of `M`. Accordingly the class :class:`TensorFreeModule` +inherits from the class +:class:`~sage.tensor.modules.finite_rank_free_module.FiniteRankFreeModule`. + +.. TODO:: + + implement more general tensor products, i.e. tensor product of the type + `M_1\otimes\cdots\otimes M_n`, where the `M_i`'s are `n` free modules of + finite rank over the same ring `R`. + +AUTHORS: + +- Eric Gourgoulhon, Michal Bejger (2014-2015): initial version + +REFERENCES: + +- K. Conrad: *Tensor products*, + `http://www.math.uconn.edu/~kconrad/blurbs/ `_ +- Chap. 21 (Exer. 4) of R. Godement: *Algebra*, Hermann (Paris) / Houghton + Mifflin (Boston) (1968) +- Chap. 16 of S. Lang: *Algebra*, 3rd ed., Springer (New York) (2002) + +""" +#****************************************************************************** +# Copyright (C) 2015 Eric Gourgoulhon +# Copyright (C) 2015 Michal Bejger +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# http://www.gnu.org/licenses/ +#****************************************************************************** + +from sage.tensor.modules.finite_rank_free_module import FiniteRankFreeModule +from sage.tensor.modules.free_module_tensor import FreeModuleTensor +from sage.tensor.modules.free_module_alt_form import FreeModuleAltForm +from sage.tensor.modules.free_module_morphism import \ + FiniteRankFreeModuleMorphism +from sage.tensor.modules.free_module_automorphism import FreeModuleAutomorphism + +class TensorFreeModule(FiniteRankFreeModule): + r""" + Class for the free modules over a commutative ring `R` that are + tensor products of a given free module `M` over `R` with itself and its + dual `M^*`: + + .. MATH:: + + T^{(k,l)}(M) = \underbrace{M\otimes\cdots\otimes M}_{k\ \; \mbox{times}} + \otimes \underbrace{M^*\otimes\cdots\otimes M^*}_{l\ \; \mbox{times}} + + As recalled above, `T^{(k,l)}(M)` can be canonically identified with the + set of tensors of type `(k,l)` on `M`. + + This is a Sage *parent* class, whose *element* class is + :class:`~sage.tensor.modules.free_module_tensor.FreeModuleTensor`. + + INPUT: + + - ``fmodule`` -- free module `M` of finite rank over a commutative ring + `R`, as an instance of + :class:`~sage.tensor.modules.finite_rank_free_module.FiniteRankFreeModule` + - ``tensor_type`` -- pair ``(k, l)`` with ``k`` being the contravariant + rank and ``l`` the covariant rank + - ``name`` -- (default: ``None``) string; name given to the tensor module + - ``latex_name`` -- (default: ``None``) string; LaTeX symbol to denote the + tensor module; if none is provided, it is set to ``name`` + + EXAMPLES: + + Set of tensors of type `(1,2)` on a free `\ZZ`-module of rank 3:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: from sage.tensor.modules.tensor_free_module import TensorFreeModule + sage: T = TensorFreeModule(M, (1,2)) ; T + Free module of type-(1,2) tensors on the + Rank-3 free module M over the Integer Ring + + Instead of importing TensorFreeModule in the global name space, it is + recommended to use the module's method + :meth:`~sage.tensor.modules.finite_rank_free_module.FiniteRankFreeModule.tensor_module`:: + + sage: T = M.tensor_module(1,2) ; T + Free module of type-(1,2) tensors on the + Rank-3 free module M over the Integer Ring + sage: latex(T) + T^{(1, 2)}\left(M\right) + + The module ``M`` itself is considered as the set of tensors of + type `(1,0)`:: + + sage: M is M.tensor_module(1,0) + True + + ``T`` is a module (actually a free module) over `\ZZ`:: + + sage: T.category() + Category of modules over Integer Ring + sage: T in Modules(ZZ) + True + sage: T.rank() + 27 + sage: T.base_ring() + Integer Ring + sage: T.base_module() + Rank-3 free module M over the Integer Ring + + ``T`` is a *parent* object, whose elements are instances of + :class:`~sage.tensor.modules.free_module_tensor.FreeModuleTensor`:: + + sage: t = T.an_element() ; t + Type-(1,2) tensor on the Rank-3 free module M over the Integer Ring + sage: from sage.tensor.modules.free_module_tensor import FreeModuleTensor + sage: isinstance(t, FreeModuleTensor) + True + sage: t in T + True + sage: T.is_parent_of(t) + True + + Elements can be constructed from ``T``. In particular, 0 yields + the zero element of ``T``:: + + sage: T(0) + Type-(1,2) tensor zero on the Rank-3 free module M over the Integer Ring + sage: T(0) is T.zero() + True + + while non-zero elements are constructed by providing their components in + a given basis:: + + sage: e + Basis (e_0,e_1,e_2) on the Rank-3 free module M over the Integer Ring + sage: comp = [[[i-j+k for k in range(3)] for j in range(3)] for i in range(3)] + sage: t = T(comp, basis=e, name='t') ; t + Type-(1,2) tensor t on the Rank-3 free module M over the Integer Ring + sage: t.comp(e)[:] + [[[0, 1, 2], [-1, 0, 1], [-2, -1, 0]], + [[1, 2, 3], [0, 1, 2], [-1, 0, 1]], + [[2, 3, 4], [1, 2, 3], [0, 1, 2]]] + sage: t.display(e) + t = e_0*e^0*e^1 + 2 e_0*e^0*e^2 - e_0*e^1*e^0 + e_0*e^1*e^2 + - 2 e_0*e^2*e^0 - e_0*e^2*e^1 + e_1*e^0*e^0 + 2 e_1*e^0*e^1 + + 3 e_1*e^0*e^2 + e_1*e^1*e^1 + 2 e_1*e^1*e^2 - e_1*e^2*e^0 + + e_1*e^2*e^2 + 2 e_2*e^0*e^0 + 3 e_2*e^0*e^1 + 4 e_2*e^0*e^2 + + e_2*e^1*e^0 + 2 e_2*e^1*e^1 + 3 e_2*e^1*e^2 + e_2*e^2*e^1 + + 2 e_2*e^2*e^2 + + An alternative is to construct the tensor from an empty list of components + and to set the nonzero components afterwards:: + + sage: t = T([], name='t') + sage: t.set_comp(e)[0,1,1] = -3 + sage: t.set_comp(e)[2,0,1] = 4 + sage: t.display(e) + t = -3 e_0*e^1*e^1 + 4 e_2*e^0*e^1 + + See the documentation of + :class:`~sage.tensor.modules.free_module_tensor.FreeModuleTensor` + for the full list of arguments that can be provided to the __call__ + operator. For instance, to contruct a tensor symmetric with respect to the + last two indices:: + + sage: t = T([], name='t', sym=(1,2)) + sage: t.set_comp(e)[0,1,1] = -3 + sage: t.set_comp(e)[2,0,1] = 4 + sage: t.display(e) # notice that t^2_{10} has be set equal to t^2_{01} by symmetry + t = -3 e_0*e^1*e^1 + 4 e_2*e^0*e^1 + 4 e_2*e^1*e^0 + + The tensor modules over a given module `M` are unique:: + + sage: T is M.tensor_module(1,2) + True + + There is a coercion map from `\Lambda^p(M^*)`, the set of alternating + forms of degree `p`, to `T^{(0,p)}(M)`:: + + sage: L2 = M.dual_exterior_power(2) ; L2 + 2nd exterior power of the dual of the Rank-3 free module M over the + Integer Ring + sage: T02 = M.tensor_module(0,2) ; T02 + Free module of type-(0,2) tensors on the Rank-3 free module M over the + Integer Ring + sage: T02.has_coerce_map_from(L2) + True + + Of course, for `p\geq 2`, there is no coercion in the reverse direction, + since not every tensor of type (0,p) is alternating:: + + sage: L2.has_coerce_map_from(T02) + False + + The coercion map `\Lambda^2(M^*)\rightarrow T^{(0,2)}(M)` in action:: + + sage: a = M.alternating_form(2, name='a') ; a + Alternating form a of degree 2 on the Rank-3 free module M over the + Integer Ring + sage: a[0,1], a[1,2] = 4, -3 + sage: a.display(e) + a = 4 e^0/\e^1 - 3 e^1/\e^2 + sage: a.parent() is L2 + True + sage: ta = T02(a) ; ta + Type-(0,2) tensor a on the Rank-3 free module M over the Integer Ring + sage: ta.display(e) + a = 4 e^0*e^1 - 4 e^1*e^0 - 3 e^1*e^2 + 3 e^2*e^1 + sage: ta.symmetries() # the antisymmetry is of course preserved + no symmetry; antisymmetry: (0, 1) + + For the degree `p=1`, there is a coercion in both directions:: + + sage: L1 = M.dual_exterior_power(1) ; L1 + Dual of the Rank-3 free module M over the Integer Ring + sage: T01 = M.tensor_module(0,1) ; T01 + Free module of type-(0,1) tensors on the Rank-3 free module M over the + Integer Ring + sage: T01.has_coerce_map_from(L1) + True + sage: L1.has_coerce_map_from(T01) + True + + The coercion map `\Lambda^1(M^*)\rightarrow T^{(0,1)}(M)` in action:: + + sage: a = M.linear_form('a') + sage: a[:] = -2, 4, 1 ; a.display(e) + a = -2 e^0 + 4 e^1 + e^2 + sage: a.parent() is L1 + True + sage: ta = T01(a) ; ta + Type-(0,1) tensor a on the Rank-3 free module M over the Integer Ring + sage: ta.display(e) + a = -2 e^0 + 4 e^1 + e^2 + + The coercion map `T^{(0,1)}(M) \rightarrow \Lambda^1(M^*)` in action:: + + sage: ta.parent() is T01 + True + sage: lta = L1(ta) ; lta + Linear form a on the Rank-3 free module M over the Integer Ring + sage: lta.display(e) + a = -2 e^0 + 4 e^1 + e^2 + sage: lta == a + True + + There is a canonical identification between tensors of type (1,1) and + endomorphisms of module `M`. Accordingly, coercion maps have been + implemented between `T^{(1,1)}(M)` and `\mathrm{End}(M)` (the module of + all endomorphisms of `M`, see + :class:`~sage.tensor.modules.free_module_homset.FreeModuleHomset`):: + + sage: T11 = M.tensor_module(1,1) ; T11 + Free module of type-(1,1) tensors on the Rank-3 free module M over the + Integer Ring + sage: End(M) + Set of Morphisms from Rank-3 free module M over the Integer Ring to + Rank-3 free module M over the Integer Ring in Category of modules + over Integer Ring + sage: T11.has_coerce_map_from(End(M)) + True + sage: End(M).has_coerce_map_from(T11) + True + + The coercion map `\mathrm{End}(M)\rightarrow T^{(1,1)}(M)` in action:: + + sage: phi = End(M).an_element() ; phi + Generic endomorphism of Rank-3 free module M over the Integer Ring + sage: phi.matrix(e) + [1 1 1] + [1 1 1] + [1 1 1] + sage: tphi = T11(phi) ; tphi # image of phi by the coercion map + Type-(1,1) tensor on the Rank-3 free module M over the Integer Ring + sage: tphi[:] + [1 1 1] + [1 1 1] + [1 1 1] + sage: t = M.tensor((1,1)) + sage: t[0,0], t[1,1], t[2,2] = -1,-2,-3 + sage: t[:] + [-1 0 0] + [ 0 -2 0] + [ 0 0 -3] + sage: s = t + phi ; s # phi is coerced to a type-(1,1) tensor prior to the addition + Type-(1,1) tensor on the Rank-3 free module M over the Integer Ring + sage: s[:] + [ 0 1 1] + [ 1 -1 1] + [ 1 1 -2] + + The coercion map `T^{(1,1)}(M) \rightarrow \mathrm{End}(M)` in action:: + + sage: phi1 = End(M)(tphi) ; phi1 + Generic endomorphism of Rank-3 free module M over the Integer Ring + sage: phi1 == phi + True + sage: s = phi + t ; s # t is coerced to an endomorphism prior to the addition + Generic endomorphism of Rank-3 free module M over the Integer Ring + sage: s.matrix(e) + [ 0 1 1] + [ 1 -1 1] + [ 1 1 -2] + + There is a coercion `\mathrm{GL}(M)\rightarrow T^{(1,1)}(M)`, i.e. from + automorphisms of `M` to type-(1,1) tensors on `M`:: + + sage: GL = M.general_linear_group() ; GL + General linear group of the Rank-3 free module M over the Integer Ring + sage: T11.has_coerce_map_from(GL) + True + + The coercion map `\mathrm{GL}(M)\rightarrow T^{(1,1)}(M)` in action:: + + sage: a = GL.an_element() ; a + Automorphism of the Rank-3 free module M over the Integer Ring + sage: a.matrix(e) + [ 1 0 0] + [ 0 -1 0] + [ 0 0 1] + sage: ta = T11(a) ; ta + Type-(1,1) tensor on the Rank-3 free module M over the Integer Ring + sage: ta.display(e) + e_0*e^0 - e_1*e^1 + e_2*e^2 + sage: a.display(e) + e_0*e^0 - e_1*e^1 + e_2*e^2 + + Of course, there is no coercion in the reverse direction, since not + every type-(1,1) tensor is invertible:: + + sage: GL.has_coerce_map_from(T11) + False + + """ + + Element = FreeModuleTensor + + def __init__(self, fmodule, tensor_type, name=None, latex_name=None): + r""" + TEST:: + + sage: from sage.tensor.modules.tensor_free_module import TensorFreeModule + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: T = TensorFreeModule(M, (2,3), name='T23', latex_name=r'T^2_3') + sage: TestSuite(T).run() + + """ + self._fmodule = fmodule + self._tensor_type = tuple(tensor_type) + rank = pow(fmodule._rank, tensor_type[0] + tensor_type[1]) + self._zero_element = 0 # provisory (to avoid infinite recursion in what + # follows) + if self._tensor_type == (0,1): # case of the dual + if name is None and fmodule._name is not None: + name = fmodule._name + '*' + if latex_name is None and fmodule._latex_name is not None: + latex_name = fmodule._latex_name + r'^*' + else: + if name is None and fmodule._name is not None: + name = 'T^' + str(self._tensor_type) + '(' + fmodule._name + \ + ')' + if latex_name is None and fmodule._latex_name is not None: + latex_name = r'T^{' + str(self._tensor_type) + r'}\left(' + \ + fmodule._latex_name + r'\right)' + FiniteRankFreeModule.__init__(self, fmodule._ring, rank, name=name, + latex_name=latex_name, + start_index=fmodule._sindex, + output_formatter=fmodule._output_formatter) + # Unique representation: + if self._tensor_type in self._fmodule._tensor_modules: + raise ValueError("the module of tensors of type {}".format( + self._tensor_type) + + " has already been created") + else: + self._fmodule._tensor_modules[self._tensor_type] = self + # Zero element + self._zero_element = self._element_constructor_(name='zero', + latex_name='0') + for basis in self._fmodule._known_bases: + self._zero_element._components[basis] = \ + self._zero_element._new_comp(basis) + # (since new components are initialized to zero) + + #### Parent Methods + + def _element_constructor_(self, comp=[], basis=None, name=None, + latex_name=None, sym=None, antisym=None): + r""" + Construct a tensor. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(QQ, 2, name='M') + sage: T = M.tensor_module(1,1) + sage: T._element_constructor_(0) is T.zero() + True + sage: e = M.basis('e') + sage: t = T._element_constructor_(comp=[[2,0],[1/2,-3]], basis=e, + ....: name='t') ; t + Type-(1,1) tensor t on the 2-dimensional vector space M over the + Rational Field + sage: t.display() + t = 2 e_0*e^0 + 1/2 e_1*e^0 - 3 e_1*e^1 + sage: t.parent() + Free module of type-(1,1) tensors on the 2-dimensional vector + space M over the Rational Field + sage: t.parent() is T + True + + """ + if comp == 0: + return self._zero_element + if isinstance(comp, FiniteRankFreeModuleMorphism): + # coercion of an endomorphism to a type-(1,1) tensor: + endo = comp # for readability + if self._tensor_type == (1,1) and endo.is_endomorphism() and \ + self._fmodule is endo.domain(): + resu = self.element_class(self._fmodule, (1,1), + name=endo._name, + latex_name=endo._latex_name) + for basis, mat in endo._matrices.iteritems(): + resu.add_comp(basis[0])[:] = mat + else: + raise TypeError("cannot coerce the {}".format(endo) + + " to an element of {}".format(self)) + elif isinstance(comp, FreeModuleAltForm): + # coercion of an alternating form to a type-(0,p) tensor: + form = comp # for readability + p = form.degree() + if self._tensor_type != (0,p) or \ + self._fmodule != form.base_module(): + raise TypeError("cannot coerce the {}".format(form) + + " to an element of {}".format(self)) + if p == 1: + asym = None + else: + asym = range(p) + resu = self.element_class(self._fmodule, (0,p), name=form._name, + latex_name=form._latex_name, + antisym=asym) + for basis, comp in form._components.iteritems(): + resu._components[basis] = comp.copy() + elif isinstance(comp, FreeModuleAutomorphism): + # coercion of an automorphism to a type-(1,1) tensor: + autom = comp # for readability + if self._tensor_type != (1,1) or \ + self._fmodule != autom.base_module(): + raise TypeError("cannot coerce the {}".format(autom) + + " to an element of {}".format(self)) + resu = self.element_class(self._fmodule, (1,1), name=autom._name, + latex_name=autom._latex_name) + for basis, comp in autom._components.iteritems(): + resu._components[basis] = comp.copy() + else: + # Standard construction: + resu = self.element_class(self._fmodule, self._tensor_type, + name=name, latex_name=latex_name, + sym=sym, antisym=antisym) + if comp: + resu.set_comp(basis)[:] = comp + return resu + + def _an_element_(self): + r""" + Construct some (unamed) element of ``self``. + + EXAMPLES:: + + sage: M = FiniteRankFreeModule(QQ, 2, name='M') + sage: T = M.tensor_module(1,1) + sage: e = M.basis('e') + sage: t = T._an_element_() ; t + Type-(1,1) tensor on the 2-dimensional vector space M over the + Rational Field + sage: t.display() + 1/2 e_0*e^0 + sage: t.parent() is T + True + sage: M.tensor_module(2,3)._an_element_().display() + 1/2 e_0*e_0*e^0*e^0*e^0 + + """ + resu = self.element_class(self._fmodule, self._tensor_type) + if self._fmodule._def_basis is not None: + sindex = self._fmodule._sindex + ind = [sindex for i in range(resu._tensor_rank)] + resu.set_comp()[ind] = self._fmodule._ring.an_element() + return resu + + def _coerce_map_from_(self, other): + r""" + Determine whether coercion to ``self`` exists from other parent. + + EXAMPLES: + + Sets of module endomorphisms coerce to type-(1,1) tensor modules:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: e = M.basis('e') + sage: M.tensor_module(1,1)._coerce_map_from_(End(M)) + True + + but not to tensor modules of other types:: + + sage: M.tensor_module(0,1)._coerce_map_from_(End(M)) + False + + and not to type-(1,1) tensor modules defined on another free module:: + + sage: N = FiniteRankFreeModule(ZZ, 3, name='N') + sage: f = N.basis('f') + sage: M.tensor_module(1,1)._coerce_map_from_(End(N)) + False + + There is no coercion if the module morphisms are not endomorphisms:: + + sage: M.tensor_module(1,1)._coerce_map_from_(Hom(M,N)) + False + + Coercion from alternating forms:: + + sage: M.tensor_module(0,1)._coerce_map_from_(M.dual_exterior_power(1)) + True + sage: M.tensor_module(0,2)._coerce_map_from_(M.dual_exterior_power(2)) + True + sage: M.tensor_module(0,2)._coerce_map_from_(M.dual_exterior_power(3)) + False + sage: M.tensor_module(0,2)._coerce_map_from_(N.dual_exterior_power(2)) + False + + """ + from free_module_homset import FreeModuleHomset + from ext_pow_free_module import ExtPowerFreeModule + from free_module_linear_group import FreeModuleLinearGroup + if isinstance(other, FreeModuleHomset): + # Coercion of an endomorphism to a type-(1,1) tensor: + if self._tensor_type == (1,1): + return other.is_endomorphism_set() and \ + self._fmodule is other.domain() + else: + return False + if isinstance(other, ExtPowerFreeModule): + # Coercion of an alternating form to a type-(0,p) tensor: + return self._tensor_type == (0, other.degree()) and \ + self._fmodule is other.base_module() + + if isinstance(other, FreeModuleLinearGroup): + # Coercion of an automorphism to a type-(1,1) tensor: + return self._tensor_type == (1,1) and \ + self._fmodule is other.base_module() + return False + + #### End of parent methods + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLE:: + + sage: M = FiniteRankFreeModule(QQ, 2, name='M') + sage: M.tensor_module(1,1) + Free module of type-(1,1) tensors on the 2-dimensional vector space + M over the Rational Field + sage: M.tensor_module(0,1) + Free module of type-(0,1) tensors on the 2-dimensional vector space + M over the Rational Field + + """ + description = "Free module of type-({},{}) tensors on the {}".format( + self._tensor_type[0], self._tensor_type[1], self._fmodule) + return description + + def base_module(self): + r""" + Return the free module on which ``self`` is constructed. + + OUTPUT: + + - instance of :class:`FiniteRankFreeModule` representing the free + module on which the tensor module is defined. + + EXAMPLE: + + Base module of a type-(1,2) tensor module:: + + sage: M = FiniteRankFreeModule(ZZ, 3, name='M') + sage: T = M.tensor_module(1,2) + sage: T.base_module() + Rank-3 free module M over the Integer Ring + sage: T.base_module() is M + True + + """ + return self._fmodule + + def tensor_type(self): + r""" + Return the tensor type of ``self``. + + OUTPUT: + + - pair `(k,l)` such that ``self`` is the module tensor product + `T^{(k,l)}(M)` + + EXAMPLE:: + + sage: M = FiniteRankFreeModule(ZZ, 3) + sage: T = M.tensor_module(1,2) + sage: T.tensor_type() + (1, 2) + + """ + return self._tensor_type diff --git a/src/sage/tensor/modules/tensor_with_indices.py b/src/sage/tensor/modules/tensor_with_indices.py new file mode 100644 index 00000000000..2ca8e265ad1 --- /dev/null +++ b/src/sage/tensor/modules/tensor_with_indices.py @@ -0,0 +1,523 @@ +r""" +Index notation for tensors + +AUTHORS: + +- Eric Gourgoulhon, Michal Bejger (2014-2015): initial version + +""" +#****************************************************************************** +# Copyright (C) 2015 Eric Gourgoulhon +# Copyright (C) 2015 Michal Bejger +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# http://www.gnu.org/licenses/ +#****************************************************************************** + +from sage.structure.sage_object import SageObject + +class TensorWithIndices(SageObject): + r""" + Index notation for tensors. + + This is a technical class to allow one to write some tensor operations + (contractions and symmetrizations) in index notation. + + INPUT: + + - ``tensor`` -- a tensor (or a tensor field) + - ``indices`` -- string containing the indices, as single letters; the + contravariant indices must be stated first and separated from the + covariant indices by the character ``_`` + + EXAMPLES: + + Index representation of tensors on a rank-3 free module:: + + sage: M = FiniteRankFreeModule(QQ, 3, name='M') + sage: e = M.basis('e') + sage: a = M.tensor((2,0), name='a') + sage: a[:] = [[1,2,3], [4,5,6], [7,8,9]] + sage: b = M.tensor((0,2), name='b') + sage: b[:] = [[-1,2,-3], [-4,5,6], [7,-8,9]] + sage: t = a*b ; t.set_name('t') ; t + Type-(2,2) tensor t on the 3-dimensional vector space M over the + Rational Field + sage: from sage.tensor.modules.tensor_with_indices import TensorWithIndices + sage: T = TensorWithIndices(t, '^ij_kl') ; T + t^ij_kl + + The :class:`TensorWithIndices` object is returned by the square + bracket operator acting on the tensor and fed with the string specifying + the indices:: + + sage: a['^ij'] + a^ij + sage: type(a['^ij']) + + sage: b['_ef'] + b_ef + sage: t['^ij_kl'] + t^ij_kl + + The symbol '^' may be omitted, since the distinction between covariant + and contravariant indices is performed by the index position relative to + the symbol '_':: + + sage: t['ij_kl'] + t^ij_kl + + Also, LaTeX notation may be used:: + + sage: t['^{ij}_{kl}'] + t^ij_kl + + If some operation is asked in the index notation, the resulting tensor + is returned, not a :class:`TensorWithIndices` object; for instance, for + a symmetrization:: + + sage: s = t['^(ij)_kl'] ; s # the symmetrization on i,j is indicated by parentheses + Type-(2,2) tensor on the 3-dimensional vector space M over the + Rational Field + sage: s.symmetries() + symmetry: (0, 1); no antisymmetry + sage: s == t.symmetrize(0,1) + True + + The letters denoting the indices can be chosen freely; since they carry no + information, they can even be replaced by dots:: + + sage: t['^(..)_..'] == t.symmetrize(0,1) + True + + Similarly, for an antisymmetrization:: + + sage: s = t['^ij_[kl]'] ; s # the symmetrization on k,l is indicated by square brackets + Type-(2,2) tensor on the 3-dimensional vector space M over the Rational + Field + sage: s.symmetries() + no symmetry; antisymmetry: (2, 3) + sage: s == t.antisymmetrize(2,3) + True + + Another example of an operation indicated by indices is a contraction:: + + sage: s = t['^ki_kj'] ; s # contraction on the repeated index k + Type-(1,1) tensor on the 3-dimensional vector space M over the Rational + Field + sage: s == t.trace(0,2) + True + + Indices not involved in the contraction may be replaced by dots:: + + sage: s == t['^k._k.'] + True + + The contraction of two tensors is indicated by repeated indices and + the ``*`` operator:: + + sage: s = a['^ik'] * b['_kj'] ; s + Type-(1,1) tensor on the 3-dimensional vector space M over the Rational + Field + sage: s == a.contract(1, b, 0) + True + sage: s = t['^.k_..'] * b['_.k'] ; s + Type-(1,3) tensor on the 3-dimensional vector space M over the Rational + Field + sage: s == t.contract(1, b, 1) + True + sage: t['^{ik}_{jl}']*b['_{mk}'] == s # LaTeX notation + True + + Contraction on two indices:: + + sage: s = a['^kl'] * b['_kl'] ; s + 105 + sage: s == a.contract(0,1, b, 0,1) + True + + Some minimal arithmetics:: + + sage: 2*a['^ij'] + X^ij + sage: (2*a['^ij'])._tensor == 2*a + True + sage: 2*t['ij_kl'] + X^ij_kl + sage: +a['^ij'] + +a^ij + sage: +t['ij_kl'] + +t^ij_kl + sage: -a['^ij'] + -a^ij + sage: -t['ij_kl'] + -t^ij_kl + + """ + def __init__(self, tensor, indices): + r""" + TESTS:: + + sage: from sage.tensor.modules.tensor_with_indices import TensorWithIndices + sage: M = FiniteRankFreeModule(QQ, 3, name='M') + sage: t = M.tensor((2,1), name='t') + sage: ti = TensorWithIndices(t, 'ab_c') + + We need to skip the pickling test because we can't check equality + unless the tensor was defined w.r.t. a basis:: + + sage: TestSuite(ti).run(skip="_test_pickling") + + sage: e = M.basis('e') + sage: t[:] = [[[1,2,3], [-4,5,6], [7,8,-9]], + ....: [[10,-11,12], [13,14,-15], [16,17,18]], + ....: [[19,-20,-21], [-22,23,24], [25,26,-27]]] + sage: ti = TensorWithIndices(t, 'ab_c') + sage: TestSuite(ti).run() + + """ + self._tensor = tensor # may be changed below + self._changed = False # indicates whether self contains an altered + # version of the original tensor (True if + # symmetries or contractions are indicated in the + # indices) + # Suppress all '{' and '}' comming from LaTeX notations: + indices = indices.replace('{','').replace('}','') + # Suppress the first '^': + if indices[0] == '^': + indices = indices[1:] + if '^' in indices: + raise IndexError("the contravariant indices must be placed first") + con_cov = indices.split('_') + con = con_cov[0] + + # Contravariant indices + # --------------------- + # Search for (anti)symmetries: + if '(' in con: + sym1 = con.index('(') + sym2 = con.index(')')-2 + if con.find('(', sym1+1) != -1 or '[' in con: + raise NotImplementedError("Multiple symmetries are not " + + "treated yet.") + self._tensor = self._tensor.symmetrize(*(range(sym1, sym2+1))) + self._changed = True # self does no longer contain the original tensor + con = con.replace('(','').replace(')','') + if '[' in con: + sym1 = con.index('[') + sym2 = con.index(']')-2 + if con.find('[', sym1+1) != -1 or '(' in con: + raise NotImplementedError("multiple symmetries are not " + + "treated yet") + self._tensor = self._tensor.antisymmetrize(*(range(sym1, sym2+1))) + self._changed = True # self does no longer contain the original tensor + con = con.replace('[','').replace(']','') + if len(con) != self._tensor._tensor_type[0]: + raise IndexError("number of contravariant indices not compatible " + + "with the tensor type") + self._con = con + + # Covariant indices + # ----------------- + if len(con_cov) == 1: + if tensor._tensor_type[1] != 0: + raise IndexError("number of covariant indices not compatible " + + "with the tensor type") + self._cov = '' + elif len(con_cov) == 2: + cov = con_cov[1] + # Search for (anti)symmetries: + if '(' in cov: + sym1 = cov.index('(') + sym2 = cov.index(')')-2 + if cov.find('(', sym1+1) != -1 or '[' in cov: + raise NotImplementedError("multiple symmetries are not " + + "treated yet") + csym1 = sym1 + self._tensor._tensor_type[0] + csym2 = sym2 + self._tensor._tensor_type[0] + self._tensor = self._tensor.symmetrize( + *(range(csym1, csym2+1))) + self._changed = True # self does no longer contain the original + # tensor + cov = cov.replace('(','').replace(')','') + if '[' in cov: + sym1 = cov.index('[') + sym2 = cov.index(']')-2 + if cov.find('[', sym1+1) != -1 or '(' in cov: + raise NotImplementedError("multiple symmetries are not " + + "treated yet") + csym1 = sym1 + self._tensor._tensor_type[0] + csym2 = sym2 + self._tensor._tensor_type[0] + self._tensor = self._tensor.antisymmetrize( + *(range(csym1, csym2+1))) + self._changed = True # self does no longer contain the original + # tensor + cov = cov.replace('[','').replace(']','') + if len(cov) != tensor._tensor_type[1]: + raise IndexError("number of covariant indices not " + + "compatible with the tensor type") + self._cov = cov + else: + raise IndexError("too many '_' in the list of indices") + + # Treatment of possible self-contractions: + # --------------------------------------- + contraction_pairs = [] + for ind in self._con: + if ind != '.' and ind in self._cov: + pos1 = self._con.index(ind) + pos2 = self._tensor._tensor_type[0] + self._cov.index(ind) + contraction_pairs.append((pos1, pos2)) + if len(contraction_pairs) > 1: + raise NotImplementedError("multiple self-contractions are not " + + "implemented yet") + if len(contraction_pairs) == 1: + pos1 = contraction_pairs[0][0] + pos2 = contraction_pairs[0][1] + self._tensor = self._tensor.trace(pos1, pos2) + self._changed = True # self does no longer contain the original + # tensor + ind = self._con[pos1] + self._con = self._con.replace(ind, '') + self._cov = self._cov.replace(ind, '') + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLES:: + + sage: from sage.tensor.modules.tensor_with_indices import TensorWithIndices + sage: M = FiniteRankFreeModule(QQ, 3, name='M') + sage: t = M.tensor((2,1), name='t') + sage: ti = TensorWithIndices(t, 'ab_c') + sage: ti._repr_() + 't^ab_c' + sage: t = M.tensor((0,2), name='t') + sage: ti = TensorWithIndices(t, '_{ij}') + sage: ti._repr_() + 't_ij' + + """ + name = 'X' + if hasattr(self._tensor, '_name'): + if self._tensor._name is not None: + name = self._tensor._name + if self._con == '': + if self._cov == '': + return 'scalar' + else: + return name + '_' + self._cov + elif self._cov == '': + return name + '^' + self._con + else: + return name + '^' + self._con + '_' + self._cov + + def update(self): + r""" + Return the tensor contains in ``self`` if it differs from that used + for creating ``self``, otherwise return ``self``. + + EXAMPLES:: + + sage: from sage.tensor.modules.tensor_with_indices import TensorWithIndices + sage: M = FiniteRankFreeModule(QQ, 3, name='M') + sage: e = M.basis('e') + sage: a = M.tensor((1,1), name='a') + sage: a[:] = [[1,-2,3], [-4,5,-6], [7,-8,9]] + sage: a_ind = TensorWithIndices(a, 'i_j') ; a_ind + a^i_j + sage: a_ind.update() + a^i_j + sage: a_ind.update() is a_ind + True + sage: a_ind = TensorWithIndices(a, 'k_k') ; a_ind + scalar + sage: a_ind.update() + 15 + + """ + if self._changed: + return self._tensor + else: + return self + + def __eq__(self, other): + """ + Check equality. + + TESTS:: + + sage: from sage.tensor.modules.tensor_with_indices import TensorWithIndices + sage: M = FiniteRankFreeModule(QQ, 3, name='M') + sage: t = M.tensor((2,1), name='t') + sage: ti = TensorWithIndices(t, 'ab_c') + sage: ti == TensorWithIndices(t, '^{ab}_c') + True + sage: ti == TensorWithIndices(t, 'ac_b') + False + sage: tp = M.tensor((2,1)) + sage: ti == TensorWithIndices(tp, 'ab_c') + Traceback (most recent call last): + ... + ValueError: no common basis for the comparison + """ + if not isinstance(other, TensorWithIndices): + return False + return (self._tensor == other._tensor + and self._con == other._con + and self._cov == other._cov) + + def __ne__(self, other): + """ + Check not equals. + + TESTS:: + + sage: from sage.tensor.modules.tensor_with_indices import TensorWithIndices + sage: M = FiniteRankFreeModule(QQ, 3, name='M') + sage: t = M.tensor((2,1), name='t') + sage: ti = TensorWithIndices(t, 'ab_c') + sage: ti != TensorWithIndices(t, '^{ab}_c') + False + sage: ti != TensorWithIndices(t, 'ac_b') + True + """ + return not self.__eq__(other) + + def __mul__(self, other): + r""" + Tensor product or contraction on specified indices. + + EXAMPLES:: + + sage: from sage.tensor.modules.tensor_with_indices import TensorWithIndices + sage: M = FiniteRankFreeModule(QQ, 3, name='M') + sage: e = M.basis('e') + sage: a = M.tensor((2,0), name='a') + sage: a[:] = [[1,-2,3], [-4,5,-6], [7,-8,9]] + sage: b = M.linear_form(name='b') + sage: b[:] = [4,2,1] + sage: ai = TensorWithIndices(a, '^ij') + sage: bi = TensorWithIndices(b, '_k') + sage: s = ai.__mul__(bi) ; s # no repeated indices ==> tensor product + Type-(2,1) tensor a*b on the 3-dimensional vector space M over the + Rational Field + sage: s == a*b + True + sage: s[:] + [[[4, 2, 1], [-8, -4, -2], [12, 6, 3]], + [[-16, -8, -4], [20, 10, 5], [-24, -12, -6]], + [[28, 14, 7], [-32, -16, -8], [36, 18, 9]]] + sage: ai = TensorWithIndices(a, '^kj') + sage: s = ai.__mul__(bi) ; s # repeated index k ==> contraction + Element of the 3-dimensional vector space M over the Rational Field + sage: s == a.contract(0, b) + True + sage: s[:] + [3, -6, 9] + + """ + if not isinstance(other, TensorWithIndices): + raise TypeError("the second item of * must be a tensor with " + + "specified indices") + contraction_pairs = [] + for ind in self._con: + if ind != '.': + if ind in other._cov: + pos1 = self._con.index(ind) + pos2 = other._tensor._tensor_type[0] + other._cov.index(ind) + contraction_pairs.append((pos1, pos2)) + if ind in other._con: + raise IndexError("the index {} appears twice ".format(ind) + + "in a contravariant position") + for ind in self._cov: + if ind != '.': + if ind in other._con: + pos1 = self._tensor._tensor_type[0] + self._cov.index(ind) + pos2 = other._con.index(ind) + contraction_pairs.append((pos1, pos2)) + if ind in other._cov: + raise IndexError("the index {} appears twice ".format(ind) + + "in a covariant position") + if not contraction_pairs: + # No contraction is performed: the tensor product is returned + return self._tensor * other._tensor + ncontr = len(contraction_pairs) + pos1 = [contraction_pairs[i][0] for i in range(ncontr)] + pos2 = [contraction_pairs[i][1] for i in range(ncontr)] + args = pos1 + [other._tensor] + pos2 + return self._tensor.contract(*args) + + def __rmul__(self, other): + r""" + Multiplication on the left by ``other``. + + EXAMPLES:: + + sage: from sage.tensor.modules.tensor_with_indices import TensorWithIndices + sage: M = FiniteRankFreeModule(QQ, 3, name='M') + sage: e = M.basis('e') + sage: a = M.tensor((2,1), name='a') + sage: a[0,2,1], a[1,2,0] = 7, -4 + sage: ai = TensorWithIndices(a, 'ij_k') + sage: s = ai.__rmul__(3) ; s + X^ij_k + sage: s._tensor == 3*a + True + + """ + return TensorWithIndices(other*self._tensor, + self._con + '_' + self._cov) + + def __pos__(self): + r""" + Unary plus operator. + + OUTPUT: + + - an exact copy of ``self`` + + EXAMPLES:: + + sage: from sage.tensor.modules.tensor_with_indices import TensorWithIndices + sage: M = FiniteRankFreeModule(QQ, 3, name='M') + sage: e = M.basis('e') + sage: a = M.tensor((2,1), name='a') + sage: a[0,2,1], a[1,2,0] = 7, -4 + sage: ai = TensorWithIndices(a, 'ij_k') + sage: s = ai.__pos__() ; s + +a^ij_k + sage: s._tensor == a + True + + """ + return TensorWithIndices(+self._tensor, + self._con + '_' + self._cov) + + def __neg__(self): + r""" + Unary minus operator. + + OUTPUT: + + - negative of ``self`` + + EXAMPLES:: + + sage: from sage.tensor.modules.tensor_with_indices import TensorWithIndices + sage: M = FiniteRankFreeModule(QQ, 3, name='M') + sage: e = M.basis('e') + sage: a = M.tensor((2,1), name='a') + sage: a[0,2,1], a[1,2,0] = 7, -4 + sage: ai = TensorWithIndices(a, 'ij_k') + sage: s = ai.__neg__() ; s + -a^ij_k + sage: s._tensor == -a + True + + """ + return TensorWithIndices(-self._tensor, + self._con + '_' + self._cov) diff --git a/src/sage/version.py b/src/sage/version.py index d60f075501c..f476e288a0f 100644 --- a/src/sage/version.py +++ b/src/sage/version.py @@ -1,4 +1,4 @@ # Sage version information for Python scripts # This file is auto-generated by the sage-update-version script, do not edit! -version = '6.6.beta5' -date = '2015-03-13' +version = '6.6.beta6' +date = '2015-03-19' diff --git a/src/sage_setup/.gitignore b/src/sage_setup/.gitignore new file mode 100644 index 00000000000..1e18f206406 --- /dev/null +++ b/src/sage_setup/.gitignore @@ -0,0 +1 @@ +/autogen/pari/timestamp diff --git a/src/sage_setup/autogen/__init__.py b/src/sage_setup/autogen/__init__.py new file mode 100644 index 00000000000..ea3e8e35c4d --- /dev/null +++ b/src/sage_setup/autogen/__init__.py @@ -0,0 +1,10 @@ +def autogen_all(force=False): + """ + Regenerate the automatically generated files of the Sage library. + + INPUT: + + - ``force`` -- whether we force rebuilding the files (default is ``False``) + """ + import pari + pari.rebuild(force=force) diff --git a/src/sage_setup/autogen/pari/__init__.py b/src/sage_setup/autogen/pari/__init__.py new file mode 100644 index 00000000000..781d09d5208 --- /dev/null +++ b/src/sage_setup/autogen/pari/__init__.py @@ -0,0 +1,19 @@ +from sage_setup.autogen.pari.generator import PariFunctionGenerator +from sage_setup.autogen.pari.parser import pari_share +import os + +def rebuild(force=False): + stamp = os.path.join(os.path.dirname(__file__), 'timestamp') + desc = os.path.join(pari_share(), 'pari.desc') + try: + if not force and os.stat(stamp).st_mtime >= os.stat(desc).st_mtime: + # No need to rebuild + return + except OSError: + pass + + print("Generating PARI functions.") + G = PariFunctionGenerator() + G() + + open(stamp, 'w').close() diff --git a/src/sage_setup/autogen/pari/args.py b/src/sage_setup/autogen/pari/args.py new file mode 100644 index 00000000000..b3b795f9055 --- /dev/null +++ b/src/sage_setup/autogen/pari/args.py @@ -0,0 +1,291 @@ +""" +Arguments for PARI calls +""" + +#***************************************************************************** +# Copyright (C) 2015 Jeroen Demeyer +# +# 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. +# http://www.gnu.org/licenses/ +#***************************************************************************** + +# Some replacements for reserved words +replacements = {'char': 'character'} + +class PariArgument(object): + """ + This class represents one argument in a PARI call. + """ + def __init__(self, namesiter, default, index): + """ + Create a new argument for a PARI call. + + INPUT: + + - ``namesiter`` -- iterator over all names of the arguments. + Usually, the next name from this iterator is used as argument + name. + + - ``default`` -- default value for this argument (``None`` + means that the argument is not optional). + + - ``index`` -- (integer >= 0). Index of this argument in the + list of arguments. Index 0 means a ``"self"`` argument which + is treated specially. For a function which is not a method, + start counting at 1. + """ + self.index = index + self.name = self.get_argument_name(namesiter) + if self.index == 0: # "self" argument can never have a default + self.default = None + elif default is None: + self.default = self.always_default() + elif default == "": + self.default = self.default_default() + else: + self.default = default + + def __repr__(self): + s = self._typerepr() + " " + self.name + if self.default is not None: + s += "=" + self.default + return s + + def _typerepr(self): + """ + Return a string representing the type of this argument. + """ + return "(generic)" + + def always_default(self): + """ + If this returns not ``None``, it is a value which is always + the default for this argument, which is then automatically + optional. + """ + return None + + def default_default(self): + """ + The default value for an optional argument if no other default + was specified in the prototype. + """ + return "NULL" + + def get_argument_name(self, namesiter): + """ + Return the name for this argument, given ``namesiter`` which is + an iterator over the argument names given by the help string. + """ + try: + n = namesiter.next() + try: + return replacements[n] + except KeyError: + return n + except StopIteration: + # No more names available, use something default. + # This is used in listcreate() for example which has a + # deprecated argument which is not listed in the help. + return "_arg%s" % self.index + + def prototype_code(self): + """ + Return code to appear in the prototype of the Cython wrapper. + """ + raise NotImplementedError + + def convert_code(self): + """ + Return code to appear in the function body to convert this + argument to something that PARI understand. This code can also + contain extra checks. + """ + return "" + + def call_code(self): + """ + Return code to put this argument in a PARI function call. + """ + return self.name + + +class PariArgumentObject(PariArgument): + """ + Class for arguments which are passed as generic Python ``object``. + """ + def __init__(self, *args, **kwds): + super(PariArgumentObject, self).__init__(*args, **kwds) + self.tmpname = "_" + self.name + + def prototype_code(self): + """ + Return code to appear in the prototype of the Cython wrapper. + """ + s = self.name + if self.default is not None: + # Default corresponds to None, actual default value should + # be handled in convert_code() + s += "=None" + return s + +class PariArgumentClass(PariArgument): + """ + Class for arguments which are passed as a specific C/Cython class. + + The C/Cython type is given by ``self.ctype()``. + """ + def ctype(self): + raise NotImplementedError + + def prototype_code(self): + """ + Return code to appear in the prototype of the Cython wrapper. + """ + s = self.ctype() + " " + self.name + if self.default is not None: + s += "=" + self.default + return s + + +class PariInstanceArgument(PariArgumentObject): + """ + ``self`` argument for ``PariInstance`` object. + """ + def __init__(self): + PariArgument.__init__(self, iter(["self"]), None, 0) + def convert_code(self): + return " cdef PariInstance pari_instance = self\n" + def _typerepr(self): + return "PariInstance" + + +class PariArgumentGEN(PariArgumentObject): + def _typerepr(self): + return "GEN" + def convert_code(self): + if self.index == 0: + # "self" is always of type gen, we skip the conversion + s = " cdef GEN {tmp} = {name}.g\n" + elif self.default is None: + s = " {name} = objtogen({name})\n" + s += " cdef GEN {tmp} = ({name}).g\n" + elif self.default == "NULL": + s = " cdef GEN {tmp} = {default}\n" + s += " if {name} is not None:\n" + s += " {name} = objtogen({name})\n" + s += " {tmp} = ({name}).g\n" + elif self.default == "0": + s = " cdef GEN {tmp}\n" + s += " if {name} is None:\n" + s += " {tmp} = gen_0\n" + s += " else:\n" + s += " {name} = objtogen({name})\n" + s += " {tmp} = ({name}).g\n" + else: + raise ValueError("default value %r for GEN argument %r is not supported" % (self.default, self.name)) + return s.format(name=self.name, tmp=self.tmpname, default=self.default) + def call_code(self): + return self.tmpname + +class PariArgumentString(PariArgumentObject): + def _typerepr(self): + return "str" + def convert_code(self): + if self.default is None: + s = " {name} = str({name})\n" + s += " cdef char* {tmp} = {name}\n" + else: + s = " cdef char* {tmp}\n" + s += " if {name} is None:\n" + s += " {tmp} = {default}\n" + s += " else:\n" + s += " {name} = bytes({name})\n" + s += " {tmp} = {name}\n" + return s.format(name=self.name, tmp=self.tmpname, default=self.default) + def call_code(self): + return self.tmpname + +class PariArgumentVariable(PariArgumentObject): + def _typerepr(self): + return "var" + def default_default(self): + return "-1" + def convert_code(self): + if self.default is None: + s = " cdef long {tmp} = pari_instance.get_var({name})\n" + else: + s = " cdef long {tmp} = {default}\n" + s += " if {name} is not None:\n" + s += " {tmp} = pari_instance.get_var({name})\n" + return s.format(name=self.name, tmp=self.tmpname, default=self.default) + def call_code(self): + return self.tmpname + +class PariArgumentLong(PariArgumentClass): + def _typerepr(self): + return "long" + def ctype(self): + return "long" + def default_default(self): + return "0" + +class PariArgumentULong(PariArgumentClass): + def _typerepr(self): + return "unsigned long" + def ctype(self): + return "unsigned long" + def default_default(self): + return "0" + +class PariArgumentPrec(PariArgumentClass): + def _typerepr(self): + return "prec" + def ctype(self): + return "long" + def always_default(self): + return "0" + def get_argument_name(self, namesiter): + return "precision" + def convert_code(self): + s = " {name} = prec_bits_to_words({name})\n" + return s.format(name=self.name) + +class PariArgumentSeriesPrec(PariArgumentClass): + def _typerepr(self): + return "serprec" + def ctype(self): + return "long" + def default_default(self): + return "-1" + def get_argument_name(self, namesiter): + return "serprec" + def convert_code(self): + s = " if {name} < 0:\n" + s += " {name} = pari_instance.get_series_precision()\n" + return s.format(name=self.name) + + +pari_arg_types = { + 'G': PariArgumentGEN, + 'r': PariArgumentString, + 's': PariArgumentString, + 'L': PariArgumentLong, + 'U': PariArgumentULong, + 'n': PariArgumentVariable, + 'p': PariArgumentPrec, + 'P': PariArgumentSeriesPrec, + + # Codes which are known but not actually supported for Sage + '&': None, + 'V': None, + 'W': None, + 'I': None, + 'E': None, + 'J': None, + 'C': None, + '*': None, + '=': None} diff --git a/src/sage_setup/autogen/pari/generator.py b/src/sage_setup/autogen/pari/generator.py new file mode 100644 index 00000000000..8a4f622ab49 --- /dev/null +++ b/src/sage_setup/autogen/pari/generator.py @@ -0,0 +1,227 @@ +""" +Auto-generate methods for PARI functions. +""" + +#***************************************************************************** +# Copyright (C) 2015 Jeroen Demeyer +# +# 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. +# http://www.gnu.org/licenses/ +#***************************************************************************** + +from __future__ import print_function +import os, re + +from sage_setup.autogen.pari.args import (PariArgumentGEN, + PariInstanceArgument) +from sage_setup.autogen.pari.parser import (sage_src_pari, + read_pari_desc, read_decl, parse_prototype) + + +gen_banner = '''# This file is auto-generated by {} + +cdef class gen_auto(RingElement): + """ + Part of the :class:`gen` class containing auto-generated functions. + + This class is not meant to be used directly, use the derived class + :class:`gen` instead. + """ +'''.format(__file__) + +instance_banner = '''# This file is auto-generated by {} + +cdef class PariInstance_auto(ParentWithBase): + """ + Part of the :class:`PariInstance` class containing auto-generated functions. + + You must never use this class directly (in fact, Sage may crash if + you do), use the derived class :class:`PariInstance` instead. + """ +'''.format(__file__) + + +function_re = re.compile(r"^[A-Za-z][A-Za-z0-9_]*$") +function_blacklist = {"O"} # O(p^e) needs special parser support + +class PariFunctionGenerator(object): + """ + Class to auto-generate ``auto_gen.pxi`` and ``auto_instance.pxi``. + + The PARI file ``pari.desc`` is read and all suitable PARI functions + are written as methods of either :class:`gen` or + :class:`PariInstance`. + """ + def __init__(self): + self.declared = read_decl() + self.gen_filename = os.path.join(sage_src_pari(), 'auto_gen.pxi') + self.instance_filename = os.path.join(sage_src_pari(), 'auto_instance.pxi') + + def can_handle_function(self, function, cname, **kwds): + """ + Can we actually handle this function in Sage? + + EXAMPLES:: + + sage: from sage_setup.autogen.pari.generator import PariFunctionGenerator + sage: G = PariFunctionGenerator() + sage: G.can_handle_function("bnfinit", "bnfinit0", **{"class":"basic"}) + True + sage: G.can_handle_function("_bnfinit", "bnfinit0", **{"class":"basic"}) + False + sage: G.can_handle_function("bnfinit", "BNFINIT0", **{"class":"basic"}) + False + sage: G.can_handle_function("bnfinit", "bnfinit0", **{"class":"hard"}) + False + """ + if function in function_blacklist: + # Blacklist specific troublesome functions + return False + if not function_re.match(function): + # Not a legal function name, like "!_" + return False + if cname not in self.declared: + # PARI function not in Sage's decl.pxi or declinl.pxi + return False + cls = kwds.get("class", "unknown") + sec = kwds.get("section", "unknown") + if cls not in ("basic", "highlevel"): + # Different class: probably something technical or + # gp2c-specific + return False + if sec == "programming/control": + # Skip if, return, break, ... + return False + return True + + def handle_pari_function(self, function, cname="", prototype="", help="", doc="", **kwds): + r""" + Handle one PARI function: decide whether or not to add the + function to Sage, in which file (as method of :class:`gen` or + of :class:`PariInstance`?) and call :meth:`write_method` to + actually write the code. + + EXAMPLES:: + + sage: from sage_setup.autogen.pari.parser import read_pari_desc + sage: from sage_setup.autogen.pari.generator import PariFunctionGenerator + sage: G = PariFunctionGenerator() + sage: G.gen_file = sys.stdout + sage: G.instance_file = sys.stdout + sage: G.handle_pari_function("bnfinit", + ....: cname="bnfinit0", prototype="GD0,L,DGp", + ....: help=r"bnfinit(P,{flag=0},{tech=[]}): compute...", + ....: doc=r"Doc: initializes a \var{bnf} structure", + ....: **{"class":"basic", "section":"number_fields"}) + def bnfinit(P, long flag=0, tech=None, long precision=0): + cdef GEN _P = P.g + cdef GEN _tech = NULL + if tech is not None: + tech = objtogen(tech) + _tech = (tech).g + precision = prec_bits_to_words(precision) + pari_catch_sig_on() + cdef GEN _ret = bnfinit0(_P, flag, _tech, precision) + return pari_instance.new_gen(_ret) + + sage: G.handle_pari_function("ellmodulareqn", + ....: cname="ellmodulareqn", prototype="LDnDn", + ....: help=r"ellmodulareqn(N,{x},{y}): return...", + ....: doc=r"return a vector [\kbd{eqn},$t$] where \kbd{eqn} is...", + ....: **{"class":"basic", "section":"elliptic_curves"}) + def ellmodulareqn(self, long N, x=None, y=None): + cdef PariInstance pari_instance = self + cdef long _x = -1 + if x is not None: + _x = pari_instance.get_var(x) + cdef long _y = -1 + if y is not None: + _y = pari_instance.get_var(y) + pari_catch_sig_on() + cdef GEN _ret = ellmodulareqn(N, _x, _y) + return pari_instance.new_gen(_ret) + + sage: G.handle_pari_function("setrand", + ....: cname="setrand", prototype="vG", + ....: help=r"setrand(n): reset the seed...", + ....: doc=r"reseeds the random number generator...", + ....: **{"class":"basic", "section":"programming/specific"}) + def setrand(n): + cdef GEN _n = n.g + pari_catch_sig_on() + setrand(_n) + pari_instance.clear_stack() + + """ + if not self.can_handle_function(function, cname, **kwds): + return + + try: + args, ret = parse_prototype(prototype, help) + except NotImplementedError: + return # Skip unsupported prototype codes + + # Is the first argument a GEN? + if len(args) > 0 and isinstance(args[0], PariArgumentGEN): + # If yes, write a method of the gen class. + cargs = args + f = self.gen_file + else: + # If no, write a method of the PariInstance class. + # Parse again with an extra "self" argument. + args, ret = parse_prototype(prototype, help, [PariInstanceArgument()]) + cargs = args[1:] + f = self.instance_file + + self.write_method(function, cname, args, ret, cargs, f) + + def write_method(self, function, cname, args, ret, cargs, file): + """ + Write Cython code with a method to call one PARI function. + + INPUT: + + - ``function`` -- name for the method + + - ``cname`` -- name of the PARI C library call + + - ``args``, ``ret`` -- output from ``parse_prototype``, + including the initial args like ``self`` + + - ``cargs`` -- like ``args`` but excluding the initial args + + - ``file`` -- a file object where the code should be written to + """ + protoargs = ", ".join(a.prototype_code() for a in args) + callargs = ", ".join(a.call_code() for a in cargs) + + s = " def {function}({protoargs}):\n" + for a in args: + s += a.convert_code() + s += " pari_catch_sig_on()\n" + s += ret.assign_code("{cname}({callargs})") + s += ret.return_code() + + s = s.format(function=function, protoargs=protoargs, cname=cname, callargs=callargs) + print (s, file=file) + + def __call__(self): + """ + Top-level function to generate the auto-generated files. + """ + D = read_pari_desc() + D = sorted(D.values(), key=lambda d: d['function']) + + self.gen_file = open(self.gen_filename, 'w') + self.gen_file.write(gen_banner) + self.instance_file = open(self.instance_filename, 'w') + self.instance_file.write(instance_banner) + + for v in D: + self.handle_pari_function(**v) + + self.gen_file.close() + self.instance_file.close() diff --git a/src/sage_setup/autogen/pari/parser.py b/src/sage_setup/autogen/pari/parser.py new file mode 100644 index 00000000000..24c7c8de253 --- /dev/null +++ b/src/sage_setup/autogen/pari/parser.py @@ -0,0 +1,209 @@ +""" +Read and parse the file pari.desc +""" + +#***************************************************************************** +# Copyright (C) 2015 Jeroen Demeyer +# +# 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. +# http://www.gnu.org/licenses/ +#***************************************************************************** + +import os, re + +from sage_setup.autogen.pari.args import pari_arg_types +from sage_setup.autogen.pari.ret import pari_ret_types + + +def sage_src_pari(): + """ + Return the directory in the Sage source tree where the interface to + the PARI library is and where the auto-generated files should be + stored. + + EXAMPLES:: + + sage: from sage_setup.autogen.pari.parser import sage_src_pari + sage: sage_src_pari() + '.../src/sage/libs/pari' + """ + SAGE_SRC = os.environ['SAGE_SRC'] + return os.path.join(SAGE_SRC, 'sage', 'libs', 'pari') + +def pari_share(): + r""" + Return the directory where the PARI data files are stored. + + EXAMPLES:: + + sage: from sage_setup.autogen.pari.parser import pari_share + sage: pari_share() + '.../local/share/pari' + """ + SAGE_LOCAL = os.environ["SAGE_LOCAL"] + return os.path.join(SAGE_LOCAL, "share", "pari") + +paren_re = re.compile(r"[(](.*)[)]") +argname_re = re.compile(r"[ {]*([A-Za-z0-9_]+)") + +def read_pari_desc(): + """ + Read and parse the file ``pari.desc``. + + The output is a dictionary where the keys are GP function names + and the corresponding values are dictionaries containing the + ``(key, value)`` pairs from ``pari.desc``. + + EXAMPLES:: + + sage: from sage_setup.autogen.pari.parser import read_pari_desc + sage: D = read_pari_desc() + sage: D["cos"] + {'class': 'basic', + 'cname': 'gcos', + 'doc': 'cosine of $x$.', + 'function': 'cos', + 'help': 'cos(x): cosine of x.', + 'prototype': 'Gp', + 'section': 'transcendental'} + """ + with open(os.path.join(pari_share(), 'pari.desc')) as f: + lines = f.readlines() + + n = 0 + N = len(lines) + + functions = {} + while n < N: + fun = {} + while True: + L = lines[n]; n += 1 + if L == "\n": + break + # As long as the next lines start with a space, append them + while lines[n].startswith(" "): + L += (lines[n])[1:]; n += 1 + key, value = L.split(":", 1) + # Change key to an allowed identifier name + key = key.lower().replace("-", "") + fun[key] = value.strip() + + name = fun["function"] + functions[name] = fun + + return functions + + +decl_re = re.compile(" ([A-Za-z][A-Za-z0-9_]*)[(]") + +def read_decl(): + """ + Read the files ``decl.pxi`` and ``declinl.pxi`` and return a set + of all declared PARI library functions. + + We do a simple regexp search, so there might be false positives. + The main use is to skip undeclared functions. + + EXAMPLES:: + + sage: from sage_setup.autogen.pari.parser import read_decl + sage: read_decl() + {'ABC_to_bnr', ..., 'zx_to_ZX'} + """ + s = set() + with open(os.path.join(sage_src_pari(), "decl.pxi")) as f: + s.update(decl_re.findall(f.read())) + with open(os.path.join(sage_src_pari(), "declinl.pxi")) as f: + s.update(decl_re.findall(f.read())) + return s + + +def parse_prototype(proto, help, initial_args=[]): + """ + Parse arguments and return type of a PARI function. + + INPUT: + + - ``proto`` -- a PARI prototype like ``"GD0,L,DGDGDG"`` + + - ``help`` -- a PARI help string like + ``"qfbred(x,{flag=0},{d},{isd},{sd})"`` + + - ``initial_args`` -- other arguments to this function which come + before the PARI arguments, for example a ``self`` argument. + + OUTPUT: a tuple ``(args, ret)`` where + + - ``args`` is a list consisting of ``initial_args`` followed by + :class:`PariArgument` instances with all arguments of this + function. + + - ``ret`` is a :class:`PariReturn` instance with the return type of + this function. + + EXAMPLES:: + + sage: from sage_setup.autogen.pari.parser import parse_prototype + sage: proto = 'GD0,L,DGDGDG' + sage: help = 'qfbred(x,{flag=0},{d},{isd},{sd})' + sage: parse_prototype(proto, help) + ([GEN x, long flag=0, GEN d=NULL, GEN isd=NULL, GEN sd=NULL], GEN) + sage: parse_prototype("lp", "foo()", ["TEST"]) + (['TEST', prec precision=0], long) + """ + # Use the help string just for the argument names. + # "names" should be an iterator over the argument names. + m = paren_re.search(help) + if m is None: + names = iter([]) + else: + s = m.groups()[0] + matches = [argname_re.match(x) for x in s.split(",")] + names = (m.groups()[0] for m in matches if m is not None) + + # First, handle the return type + try: + c = proto[0] + t = pari_ret_types[c] + n = 1 # index in proto + except (IndexError, KeyError): + t = pari_ret_types[""] + n = 0 # index in proto + ret = t() + + # Go over the prototype characters and build up the arguments + args = list(initial_args) + default = None + while n < len(proto): + c = proto[n]; n += 1 + + # Parse default value + if c == "D": + default = "" + if proto[n] not in pari_arg_types: + while True: + c = proto[n]; n += 1 + if c == ",": + break + default += c + c = proto[n]; n += 1 + else: + default = None + + try: + t = pari_arg_types[c] + if t is None: + raise NotImplementedError('unsupported prototype character %r' % c) + except KeyError: + if c == ",": + continue # Just skip additional commas + else: + raise ValueError('unknown prototype character %r' % c) + + arg = t(names, default, index=len(args)) + args.append(arg) + + return (args, ret) diff --git a/src/sage_setup/autogen/pari/ret.py b/src/sage_setup/autogen/pari/ret.py new file mode 100644 index 00000000000..6555765bedb --- /dev/null +++ b/src/sage_setup/autogen/pari/ret.py @@ -0,0 +1,84 @@ +""" +Return types for PARI calls +""" + +#***************************************************************************** +# Copyright (C) 2015 Jeroen Demeyer +# +# 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. +# http://www.gnu.org/licenses/ +#***************************************************************************** + +class PariReturn(object): + """ + This class represents the return value of a PARI call. + """ + def __init__(self): + self.name = "_ret" + + def __repr__(self): + return self.ctype() + + def ctype(self): + """ + Return the C type of the result of the PARI call. + """ + raise NotImplementedError + + def assign_code(self, value): + """ + Return code to assign the result of the PARI call in ``value`` + to the variable named ``self.name``. + """ + s = " cdef {ctype} {name} = {value}\n" + return s.format(ctype=self.ctype(), name=self.name, value=value) + + def return_code(self): + """ + Return code to return from the Cython wrapper. + """ + s = " pari_instance.clear_stack()\n" + s += " return {name}\n" + return s.format(name=self.name) + + +class PariReturnGEN(PariReturn): + def ctype(self): + return "GEN" + def return_code(self): + s = " return pari_instance.new_gen({name})\n" + return s.format(name=self.name) + +class PariReturnInt(PariReturn): + def ctype(self): + return "int" + +class PariReturnLong(PariReturn): + def ctype(self): + return "long" + +class PariReturnULong(PariReturn): + def ctype(self): + return "unsigned long" + +class PariReturnVoid(PariReturn): + def ctype(self): + return "void" + def assign_code(self, value): + return " {value}\n".format(value=value) + def return_code(self): + s = " pari_instance.clear_stack()\n" + return s + + +pari_ret_types = { + '': PariReturnGEN, + 'm': PariReturnGEN, + 'i': PariReturnInt, + 'l': PariReturnLong, + 'u': PariReturnULong, + 'v': PariReturnVoid, + } diff --git a/src/sage_setup/find.py b/src/sage_setup/find.py index 56c4d6f76ad..d208364601b 100644 --- a/src/sage_setup/find.py +++ b/src/sage_setup/find.py @@ -48,7 +48,7 @@ def find_python_sources(src_dir, modules=('sage',)): 1 loops, best of 1: 18.8 ms per loop sage: find_python_sources(SAGE_SRC, modules=['sage_setup']) - (['sage_setup'], [...'sage_setup.find'...]) + (['sage_setup', ...], [...'sage_setup.find'...]) """ PYMOD_EXT = os.path.extsep + 'py' INIT_FILE = '__init__' + PYMOD_EXT diff --git a/src/setup.py b/src/setup.py index 76b3aa7594c..438b91e7cef 100644 --- a/src/setup.py +++ b/src/setup.py @@ -68,10 +68,13 @@ extra_compile_args.append('-fno-tree-dominator-opts') # Generate interpreters - sage.ext.gen_interpreters.rebuild(os.path.join(SAGE_SRC, 'sage', 'ext', 'interpreters')) ext_modules = ext_modules + sage.ext.gen_interpreters.modules +# Generate auto-generated files +from sage_setup.autogen import autogen_all +autogen_all() + ######################################################### ### Testing related stuff